diff --git a/docs/docs/users/reference/env_variables.md b/docs/docs/users/reference/env_variables.md index d1d9bf157d73..2942270a0bfb 100644 --- a/docs/docs/users/reference/env_variables.md +++ b/docs/docs/users/reference/env_variables.md @@ -32,12 +32,13 @@ process. | `FOREST_F3_SIDECAR_RPC_ENDPOINT` | string | 127.0.0.1:23456 | `127.0.0.1:23456` | An RPC endpoint of F3 sidecar. | | `FOREST_F3_SIDECAR_FFI_ENABLED` | 1 or true | hard-coded per chain | 1 | Whether or not to start the F3 sidecar via FFI | | `FOREST_F3_CONSENSUS_ENABLED` | 1 or true | hard-coded per chain | 1 | Whether or not to apply the F3 consensus to the node | -| `FOREST_F3_MANIFEST_SERVER` | string | empty | `12D3KooWENMwUF9YxvQxar7uBWJtZkA6amvK4xWmKXfSiHUo2Qq7` | Set dynamic F3 manifest server | | `FOREST_F3_FINALITY` | integer | inherited from chain configuration | 900 | Set the chain finality epochs in F3 manifest | | `FOREST_F3_PERMANENT_PARTICIPATING_MINER_ADDRESSES` | comma delimited strings | empty | `t0100,t0101` | Set the miner addresses that participate in F3 permanently | | `FOREST_F3_INITIAL_POWER_TABLE` | string | empty | `bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i` | Set the F3 initial power table CID | | `FOREST_F3_ROOT` | string | [FOREST_DATA_ROOT]/f3 | `/var/tmp/f3` | Set the data directory for F3 | | `FOREST_F3_BOOTSTRAP_EPOCH` | integer | -1 | 100 | Set the bootstrap epoch for F3 | +| `FOREST_F3_CONTRACT_ADDRESS` | string | empty | `0x476AC9256b9921C9C6a0fC237B7fE05fe9874F50` | Set the manifest contract Ethereum address for F3 | +| `FOREST_F3_MANIFEST_POLL_INTERVAL` | string | empty | `15m` | Set the contract manifest poll interval for F3 | | `FOREST_DRAND_MAINNET_CONFIG` | string | empty | refer to Drand config format section | Override `DRAND_MAINNET` config | | `FOREST_DRAND_QUICKNET_CONFIG` | string | empty | refer to Drand config format section | Override `DRAND_QUICKNET` config | diff --git a/f3-sidecar/README.md b/f3-sidecar/README.md index 99d40421919f..19739287ff16 100644 --- a/f3-sidecar/README.md +++ b/f3-sidecar/README.md @@ -64,6 +64,3 @@ environment variable `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT=1` is set. F3 sidecar is not started by default, set `FOREST_F3_SIDECAR_FFI_ENABLED=1` to opt in. - -Set dynamic manifest server via `FOREST_F3_MANIFEST_SERVER`, e.g. -`FOREST_F3_MANIFEST_SERVER=12D3KooWENMwUF9YxvQxar7uBWJtZkA6amvK4xWmKXfSiHUo2Qq7` diff --git a/f3-sidecar/api.go b/f3-sidecar/api.go index 59c22743965c..1f116f698ffb 100644 --- a/f3-sidecar/api.go +++ b/f3-sidecar/api.go @@ -21,6 +21,7 @@ type F3Api struct { GetParticipatingMinerIDs func(context.Context) ([]uint64, error) SignMessage func(context.Context, []byte, []byte) (*crypto.Signature, error) Finalize func(context.Context, gpbft.TipSetKey) error + GetManifestFromContract func(context.Context) (*manifest.Manifest, error) } type FilecoinApi struct { diff --git a/f3-sidecar/ffi_gen.go b/f3-sidecar/ffi_gen.go index bbe3bae9aff6..62001d50db17 100644 --- a/f3-sidecar/ffi_gen.go +++ b/f3-sidecar/ffi_gen.go @@ -29,11 +29,11 @@ import ( var GoF3NodeImpl GoF3Node type GoF3Node interface { - run(rpc_endpoint *string, jwt *string, f3_rpc_endpoint *string, initial_power_table *string, bootstrap_epoch *int64, finality *int64, f3_root *string, manifest_server *string) bool + run(rpc_endpoint *string, jwt *string, f3_rpc_endpoint *string, initial_power_table *string, bootstrap_epoch *int64, finality *int64, f3_root *string, contract_manifest_poll_interval_seconds *uint64) bool } //export CGoF3Node_run -func CGoF3Node_run(rpc_endpoint C.StringRef, jwt C.StringRef, f3_rpc_endpoint C.StringRef, initial_power_table C.StringRef, bootstrap_epoch C.int64_t, finality C.int64_t, f3_root C.StringRef, manifest_server C.StringRef, slot *C.void, cb *C.void) { +func CGoF3Node_run(rpc_endpoint C.StringRef, jwt C.StringRef, f3_rpc_endpoint C.StringRef, initial_power_table C.StringRef, bootstrap_epoch C.int64_t, finality C.int64_t, f3_root C.StringRef, contract_manifest_poll_interval_seconds C.uint64_t, slot *C.void, cb *C.void) { _new_rpc_endpoint := newString(rpc_endpoint) _new_jwt := newString(jwt) _new_f3_rpc_endpoint := newString(f3_rpc_endpoint) @@ -41,8 +41,8 @@ func CGoF3Node_run(rpc_endpoint C.StringRef, jwt C.StringRef, f3_rpc_endpoint C. _new_bootstrap_epoch := newC_int64_t(bootstrap_epoch) _new_finality := newC_int64_t(finality) _new_f3_root := newString(f3_root) - _new_manifest_server := newString(manifest_server) - resp := GoF3NodeImpl.run(&_new_rpc_endpoint, &_new_jwt, &_new_f3_rpc_endpoint, &_new_initial_power_table, &_new_bootstrap_epoch, &_new_finality, &_new_f3_root, &_new_manifest_server) + _new_contract_manifest_poll_interval_seconds := newC_uint64_t(contract_manifest_poll_interval_seconds) + resp := GoF3NodeImpl.run(&_new_rpc_endpoint, &_new_jwt, &_new_f3_rpc_endpoint, &_new_initial_power_table, &_new_bootstrap_epoch, &_new_finality, &_new_f3_root, &_new_contract_manifest_poll_interval_seconds) resp_ref, buffer := cvt_ref(cntC_bool, refC_bool)(&resp) asmcall.CallFuncG0P2(unsafe.Pointer(cb), unsafe.Pointer(&resp_ref), unsafe.Pointer(slot)) runtime.KeepAlive(resp_ref) diff --git a/f3-sidecar/ffi_impl.go b/f3-sidecar/ffi_impl.go index f62638e468eb..66199520dfe2 100644 --- a/f3-sidecar/ffi_impl.go +++ b/f3-sidecar/ffi_impl.go @@ -28,12 +28,14 @@ type f3Impl struct { ctx context.Context } -func (f3 *f3Impl) run(rpc_endpoint *string, jwt *string, f3_rpc_endpoint *string, initial_power_table *string, bootstrap_epoch *int64, finality *int64, db *string, manifest_server *string) bool { +// The nil checks of the parameters are ommitted because they are passed from Rust code which are not nil. +// The signature pointer types that are generated by rust2go to avoid lifetime issues +func (f3 *f3Impl) run(rpc_endpoint *string, jwt *string, f3_rpc_endpoint *string, initial_power_table *string, bootstrap_epoch *int64, finality *int64, db *string, contract_manifest_poll_interval_seconds *uint64) bool { var err error = nil const MAX_RETRY int = 5 nRetry := 0 for nRetry <= MAX_RETRY { - err = run(f3.ctx, *rpc_endpoint, *jwt, *f3_rpc_endpoint, *initial_power_table, *bootstrap_epoch, *finality, *db, *manifest_server) + err = run(f3.ctx, *rpc_endpoint, *jwt, *f3_rpc_endpoint, *initial_power_table, *bootstrap_epoch, *finality, *db, *contract_manifest_poll_interval_seconds) if err != nil { nRetry += 1 logger.Errorf("Unexpected F3 failure, retrying(%d) in 10s... error=%s", nRetry, err) diff --git a/f3-sidecar/go.mod b/f3-sidecar/go.mod index 26d51d407737..06de75a98483 100644 --- a/f3-sidecar/go.mod +++ b/f3-sidecar/go.mod @@ -8,7 +8,6 @@ require ( github.com/filecoin-project/go-state-types v0.15.0 github.com/ihciah/rust2go v0.0.0-20250125181647-c5957947a3c0 github.com/ipfs/go-cid v0.5.0 - github.com/ipfs/go-datastore v0.6.0 github.com/ipfs/go-ds-leveldb v0.5.0 github.com/ipfs/go-log/v2 v2.5.1 github.com/libp2p/go-libp2p v0.40.0 @@ -52,6 +51,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huin/goupnp v1.3.0 // indirect github.com/ipfs/boxo v0.27.4 // indirect + github.com/ipfs/go-datastore v0.6.0 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect diff --git a/f3-sidecar/main.go b/f3-sidecar/main.go index 9cfeb590ab36..57520c443d99 100644 --- a/f3-sidecar/main.go +++ b/f3-sidecar/main.go @@ -41,13 +41,14 @@ func main() { flag.Int64Var(&finality, "finality", 900, "chain finality epochs") var root string flag.StringVar(&root, "root", "f3-data", "path to the f3 data directory") - var manifestServer string - flag.StringVar(&manifestServer, "manifest-server", "", "the peer id of the dynamic manifest server") + var contract_poll_interval uint64 + flag.Uint64Var(&contract_poll_interval, "contract-poll-interval", 900, "contract manifest poll interval seconds") + flag.Parse() ctx := context.Background() - err := run(ctx, rpcEndpoint, jwt, f3RpcEndpoint, initialPowerTable, bootstrapEpoch, finality, root, manifestServer) + err := run(ctx, rpcEndpoint, jwt, f3RpcEndpoint, initialPowerTable, bootstrapEpoch, finality, root, contract_poll_interval) if err != nil { panic(err) } diff --git a/f3-sidecar/manifest.go b/f3-sidecar/manifest.go new file mode 100644 index 000000000000..2e1ba3447c0b --- /dev/null +++ b/f3-sidecar/manifest.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "time" + + "github.com/filecoin-project/go-f3/manifest" +) + +type ContractManifestProvider struct { + started *bool + pollInterval time.Duration + initialManifest *manifest.Manifest + currentManifest *manifest.Manifest + f3Api *F3Api + ch chan *manifest.Manifest +} + +func NewContractManifestProvider(initialValue *manifest.Manifest, contract_manifest_poll_interval_seconds uint64, f3Api *F3Api) (*ContractManifestProvider, error) { + started := false + pollInterval := time.Duration(contract_manifest_poll_interval_seconds) * time.Second + p := ContractManifestProvider{ + started: &started, + pollInterval: pollInterval, + initialManifest: initialValue, + currentManifest: nil, + f3Api: f3Api, + ch: make(chan *manifest.Manifest), + } + return &p, nil +} + +func (p *ContractManifestProvider) Update(m *manifest.Manifest) { + err := m.Validate() + if err == nil { + p.currentManifest = m + p.ch <- m + } else { + logger.Warnf("Invalid manifest, skip updating, %s\n", err) + } +} + +func (p *ContractManifestProvider) Start(ctx context.Context) error { + if *p.started { + logger.Warnln("ContractManifestProvider has already been started") + return nil + } + + started := true + p.started = &started + go func() { + for started && ctx.Err() == nil { + if p.currentManifest == nil && p.initialManifest != nil { + p.Update(p.initialManifest) + p.initialManifest = nil + } + logger.Debugf("Polling manifest from contract...\n") + m, err := p.f3Api.GetManifestFromContract(ctx) + if err == nil { + if m != nil { + if !m.Equal(p.currentManifest) { + logger.Infoln("Successfully polled manifest from contract, updating...") + p.Update(m) + } else { + logger.Infoln("Successfully polled unchanged manifest from contract") + } + } + } else { + logger.Warnf("failed to get manifest from contract: %s\n", err) + } + time.Sleep(p.pollInterval) + } + }() + + return nil +} +func (p *ContractManifestProvider) Stop(context.Context) error { + *p.started = false + return nil +} +func (p *ContractManifestProvider) ManifestUpdates() <-chan *manifest.Manifest { return p.ch } diff --git a/f3-sidecar/run.go b/f3-sidecar/run.go index f55322d950b8..651252bdb86a 100644 --- a/f3-sidecar/run.go +++ b/f3-sidecar/run.go @@ -15,13 +15,10 @@ import ( "github.com/filecoin-project/go-f3/manifest" "github.com/filecoin-project/go-jsonrpc" "github.com/ipfs/go-cid" - "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/namespace" leveldb "github.com/ipfs/go-ds-leveldb" - "github.com/libp2p/go-libp2p/core/peer" ) -func run(ctx context.Context, rpcEndpoint string, jwt string, f3RpcEndpoint string, initialPowerTable string, bootstrapEpoch int64, finality int64, f3Root string, manifestServer string) error { +func run(ctx context.Context, rpcEndpoint string, jwt string, f3RpcEndpoint string, initialPowerTable string, bootstrapEpoch int64, finality int64, f3Root string, contract_manifest_poll_interval_seconds uint64) error { api := FilecoinApi{} isJwtProvided := len(jwt) > 0 closer, err := jsonrpc.NewClient(context.Background(), rpcEndpoint, "Filecoin", &api, nil) @@ -29,9 +26,16 @@ func run(ctx context.Context, rpcEndpoint string, jwt string, f3RpcEndpoint stri return err } defer closer() - network, err := api.StateNetworkName(ctx) - if err != nil { - return err + var network string + for { + network, err = api.StateNetworkName(ctx) + if err == nil { + logger.Infoln("Forest RPC server is online") + break + } else { + logger.Warnln("waiting for Forest RPC server") + time.Sleep(5 * time.Second) + } } listenAddrs, err := api.NetAddrsListen(ctx) if err != nil { @@ -80,54 +84,17 @@ func run(ctx context.Context, rpcEndpoint string, jwt string, f3RpcEndpoint stri m.CatchUpAlignment = blockDelay / 2 m.CertificateExchange.MinimumPollInterval = blockDelay m.CertificateExchange.MaximumPollInterval = 4 * blockDelay + m.EC.Finality = finality + m.BootstrapEpoch = bootstrapEpoch + m.CommitteeLookback = manifest.DefaultCommitteeLookback - head, err := ec.GetHead(ctx) + manifestProvider, err := NewContractManifestProvider(m, contract_manifest_poll_interval_seconds, &ec.f3api) if err != nil { return err } - m.EC.Finality = finality - if bootstrapEpoch < 0 { - // This is temporary logic to make the dummy bootstrap epoch work locally. - // It should be removed once bootstrapEpochs are determinted. - m.BootstrapEpoch = max(m.EC.Finality+1, head.Epoch()-m.EC.Finality+1) - } else { - m.BootstrapEpoch = bootstrapEpoch - } - m.CommitteeLookback = manifest.DefaultCommitteeLookback - - var manifestProvider manifest.ManifestProvider - switch manifestServerID, err := peer.Decode(manifestServer); { - case err != nil: - logger.Info("Using static manifest provider") - if manifestProvider, err = manifest.NewStaticManifestProvider(m); err != nil { - return err - } - default: - logger.Infof("Using dynamic manifest provider at %s", manifestServerID) - manifestDS := namespace.Wrap(ds, datastore.NewKey("/f3-dynamic-manifest")) - primaryNetworkName := m.NetworkName - filter := func(m *manifest.Manifest) error { - if m.EC.Finalize { - return fmt.Errorf("refusing dynamic manifest that finalizes tipsets") - } - if m.NetworkName == primaryNetworkName { - return fmt.Errorf( - "refusing dynamic manifest with network name %q that clashes with initial manifest", - primaryNetworkName, - ) - } - return nil - } - if manifestProvider, err = manifest.NewDynamicManifestProvider( - p2p.PubSub, - manifestServerID, - manifest.DynamicManifestProviderWithInitialManifest(m), - manifest.DynamicManifestProviderWithDatastore(manifestDS), - manifest.DynamicManifestProviderWithFilter(filter)); err != nil { - return err - } + if err := manifestProvider.Start(ctx); err != nil { + return err } - f3Module, err := f3.New(ctx, manifestProvider, ds, p2p.Host, p2p.PubSub, verif, &ec, f3Root) if err != nil { diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index d69ef7fcdeb7..95ce0a062fd1 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -603,7 +603,6 @@ fn maybe_start_f3_service( chain_finality, bootstrap_epoch, initial_power_table, - manifest_server, } = crate::f3::get_f3_sidecar_params(&chain_config); move || { crate::f3::run_f3_sidecar_if_enabled( @@ -616,7 +615,6 @@ fn maybe_start_f3_service( chain_finality, std::env::var("FOREST_F3_ROOT") .unwrap_or(default_f3_root.display().to_string()), - manifest_server.map(|i| i.to_string()).unwrap_or_default(), ); Ok(()) } diff --git a/src/f3/go_ffi.rs b/src/f3/go_ffi.rs index fe3a8b125b36..1df539a999b8 100644 --- a/src/f3/go_ffi.rs +++ b/src/f3/go_ffi.rs @@ -17,6 +17,6 @@ pub trait GoF3Node { bootstrap_epoch: i64, finality: i64, f3_root: String, - manifest_server: String, + contract_manifest_poll_interval_seconds: u64, ) -> bool; } diff --git a/src/f3/mod.rs b/src/f3/mod.rs index 68ca7f446e54..eba72dc7659a 100644 --- a/src/f3/mod.rs +++ b/src/f3/mod.rs @@ -9,7 +9,6 @@ mod go_ffi; use go_ffi::*; use cid::Cid; -use libp2p::PeerId; use crate::{networks::ChainConfig, utils::misc::env::is_env_set_and_truthy}; @@ -18,7 +17,6 @@ pub struct F3Options { pub chain_finality: i64, pub bootstrap_epoch: i64, pub initial_power_table: Cid, - pub manifest_server: Option, } pub fn get_f3_sidecar_params(chain_config: &ChainConfig) -> F3Options { @@ -55,34 +53,11 @@ pub fn get_f3_sidecar_params(chain_config: &ChainConfig) -> F3Options { tracing::info!("Using F3 bootstrap epoch {i} set by FOREST_F3_BOOTSTRAP_EPOCH") }) .unwrap_or(chain_config.f3_bootstrap_epoch); - let manifest_server = match std::env::var("FOREST_F3_MANIFEST_SERVER") { - Ok(v) => { - if v.is_empty() { - None - } else { - match v.parse() { - Ok(i) => Some(i), - _ => { - tracing::warn!( - "Invalid libp2p peer id {v} set by FOREST_F3_MANIFEST_SERVER" - ); - None - } - } - .inspect(|i| { - tracing::info!("Using F3 manifest server {i} set by FOREST_F3_MANIFEST_SERVER") - }) - .or(chain_config.f3_manifest_server) - } - } - _ => chain_config.f3_manifest_server, - }; F3Options { chain_finality, bootstrap_epoch, initial_power_table, - manifest_server, } } @@ -95,7 +70,6 @@ pub fn run_f3_sidecar_if_enabled( _bootstrap_epoch: i64, _finality: i64, _f3_root: String, - _manifest_server: String, ) { if is_sidecar_ffi_enabled(chain_config) { #[cfg(all(f3sidecar, not(feature = "no-f3-sidecar")))] @@ -109,7 +83,7 @@ pub fn run_f3_sidecar_if_enabled( _bootstrap_epoch, _finality, _f3_root, - _manifest_server, + chain_config.f3_contract_poll_interval().as_secs(), ); } } @@ -147,7 +121,6 @@ mod tests { chain_finality: chain_config.policy.chain_finality, bootstrap_epoch: chain_config.f3_bootstrap_epoch, initial_power_table: chain_config.f3_initial_power_table, - manifest_server: chain_config.f3_manifest_server, } ); @@ -158,29 +131,6 @@ mod tests { "bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i", ); std::env::set_var("FOREST_F3_BOOTSTRAP_EPOCH", "100"); - // mainnet server - std::env::set_var( - "FOREST_F3_MANIFEST_SERVER", - "12D3KooWENMwUF9YxvQxar7uBWJtZkA6amvK4xWmKXfSiHUo2Qq7", - ); - assert_eq!( - get_f3_sidecar_params(&chain_config), - F3Options { - chain_finality: 100, - bootstrap_epoch: 100, - initial_power_table: "bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i" - .parse() - .unwrap(), - manifest_server: Some( - "12D3KooWENMwUF9YxvQxar7uBWJtZkA6amvK4xWmKXfSiHUo2Qq7" - .parse() - .unwrap() - ), - } - ); - - // Unset FOREST_F3_MANIFEST_SERVER - std::env::set_var("FOREST_F3_MANIFEST_SERVER", ""); assert_eq!( get_f3_sidecar_params(&chain_config), F3Options { @@ -189,7 +139,6 @@ mod tests { initial_power_table: "bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i" .parse() .unwrap(), - manifest_server: None, } ); } diff --git a/src/networks/mod.rs b/src/networks/mod.rs index e0e518e20496..916b494032b2 100644 --- a/src/networks/mod.rs +++ b/src/networks/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use std::str::FromStr; +use std::time::Duration; use ahash::HashMap; use cid::Cid; @@ -16,6 +17,7 @@ use tracing::warn; use crate::beacon::{BeaconPoint, BeaconSchedule, DrandBeacon, DrandConfig}; use crate::db::SettingsStore; use crate::eth::EthChainId; +use crate::rpc::eth::types::EthAddress; use crate::shim::clock::{ChainEpoch, EPOCHS_IN_DAY, EPOCH_DURATION_SECONDS}; use crate::shim::sector::{RegisteredPoStProofV3, RegisteredSealProofV3}; use crate::shim::version::NetworkVersion; @@ -43,6 +45,7 @@ pub const NEWEST_NETWORK_VERSION: NetworkVersion = NetworkVersion::V17; const ENV_FOREST_BLOCK_DELAY_SECS: &str = "FOREST_BLOCK_DELAY_SECS"; const ENV_FOREST_PROPAGATION_DELAY_SECS: &str = "FOREST_PROPAGATION_DELAY_SECS"; const ENV_PLEDGE_RULE_RAMP: &str = "FOREST_PLEDGE_RULE_RAMP"; +const DEFAULT_F3_CONTRACT_POLL_INTERVAL: Duration = Duration::from_secs(15 * 60); /// Forest builtin `filecoin` network chains. In general only `mainnet` and its /// chain information should be considered stable. @@ -235,9 +238,9 @@ pub struct ChainConfig { pub f3_consensus: bool, pub f3_bootstrap_epoch: i64, pub f3_initial_power_table: Cid, - // This will likely be deprecated once F3 is fully bootstrapped to avoid single point network dependencies. - #[cfg_attr(test, arbitrary(gen(|_| Some(libp2p::PeerId::random()))))] - pub f3_manifest_server: Option, + #[cfg_attr(test, arbitrary(gen(|_| Some(EthAddress::from_str("0x476AC9256b9921C9C6a0fC237B7fE05fe9874F50").unwrap()))))] + f3_contract_address: Option, + f3_contract_poll_interval: Duration, } impl ChainConfig { @@ -259,15 +262,15 @@ impl ChainConfig { breeze_gas_tamping_duration: BREEZE_GAS_TAMPING_DURATION, // 1 year on mainnet fip0081_ramp_duration_epochs: 365 * EPOCHS_IN_DAY as u64, - f3_enabled: false, - f3_consensus: false, + f3_enabled: true, + f3_consensus: true, f3_bootstrap_epoch: -1, f3_initial_power_table: Default::default(), - f3_manifest_server: Some( - "12D3KooWENMwUF9YxvQxar7uBWJtZkA6amvK4xWmKXfSiHUo2Qq7" - .parse() - .expect("Invalid PeerId"), + f3_contract_address: Some( + EthAddress::from_str("0x476AC9256b9921C9C6a0fC237B7fE05fe9874F50") + .expect("invalid f3 contract eth address"), ), + f3_contract_poll_interval: DEFAULT_F3_CONTRACT_POLL_INTERVAL, } } @@ -299,11 +302,8 @@ impl ChainConfig { "bafy2bzaceab236vmmb3n4q4tkvua2n4dphcbzzxerxuey3mot4g3cov5j3r2c" .parse() .expect("invalid f3_initial_power_table"), - f3_manifest_server: Some( - "12D3KooWS9vD9uwm8u2uPyJV32QBAhKAmPYwmziAgr3Xzk2FU1Mr" - .parse() - .expect("Invalid PeerId"), - ), + f3_contract_address: Default::default(), + f3_contract_poll_interval: DEFAULT_F3_CONTRACT_POLL_INTERVAL, } } @@ -326,7 +326,8 @@ impl ChainConfig { f3_consensus: false, f3_bootstrap_epoch: -1, f3_initial_power_table: Default::default(), - f3_manifest_server: None, + f3_contract_address: Default::default(), + f3_contract_poll_interval: DEFAULT_F3_CONTRACT_POLL_INTERVAL, } } @@ -355,11 +356,8 @@ impl ChainConfig { f3_consensus: true, f3_bootstrap_epoch: -1, f3_initial_power_table: Default::default(), - f3_manifest_server: Some( - "12D3KooWJr9jy4ngtJNR7JC1xgLFra3DjEtyxskRYWvBK9TC3Yn6" - .parse() - .expect("Invalid PeerId"), - ), + f3_contract_address: Default::default(), + f3_contract_poll_interval: DEFAULT_F3_CONTRACT_POLL_INTERVAL, } } @@ -449,6 +447,38 @@ impl ChainConfig { pub fn genesis_network_version(&self) -> NetworkVersion { self.genesis_network } + + #[allow(dead_code)] + pub fn f3_contract_poll_interval(&self) -> Duration { + const ENV_KEY: &str = "FOREST_F3_MANIFEST_POLL_INTERVAL"; + std::env::var(ENV_KEY) + .ok() + .and_then(|i| humantime::Duration::from_str(&i).ok()) + .inspect(|i| { + tracing::info!("Using F3 contract manifest poll interval {i} set by {ENV_KEY}") + }) + .map(Into::into) + .unwrap_or(self.f3_contract_poll_interval) + } + + pub fn f3_contract_address(&self) -> Option { + const ENV_KEY: &str = "FOREST_F3_CONTRACT_ADDRESS"; + std::env::var(ENV_KEY) + .ok() + .and_then(|i| { + if i.is_empty() { + tracing::info!("F3 contract is disabled by {ENV_KEY}"); + None + } else if let Ok(addr) = EthAddress::from_str(&i) { + tracing::info!("Using F3 contract address {i} set by {ENV_KEY}"); + Some(addr) + } else { + tracing::warn!("Failed to parse F3 contract address {i}"); + None + } + }) + .or_else(|| self.f3_contract_address.clone()) + } } impl Default for ChainConfig { diff --git a/src/rpc/methods/f3.rs b/src/rpc/methods/f3.rs index 55c32f085ebb..5033618652ae 100644 --- a/src/rpc/methods/f3.rs +++ b/src/rpc/methods/f3.rs @@ -12,35 +12,44 @@ mod util; pub use self::types::{F3Instant, F3LeaseManager, F3Manifest, F3PowerEntry, FinalityCertificate}; use self::{types::*, util::*}; -use super::wallet::WalletSign; -use crate::shim::actors::{ - convert::{ - from_policy_v13_to_v10, from_policy_v13_to_v11, from_policy_v13_to_v12, - from_policy_v13_to_v14, from_policy_v13_to_v15, from_policy_v13_to_v16, - from_policy_v13_to_v9, - }, - miner, power, -}; +use super::{eth::types::EthAddress, wallet::WalletSign}; use crate::{ blocks::Tipset, chain::index::ResolveNullTipset, chain_sync::TipsetValidator, libp2p::{NetRPCMethods, NetworkMessage}, lotus_json::HasLotusJson as _, - rpc::{types::ApiTipsetKey, ApiPaths, Ctx, Permission, RpcMethod, ServerError}, + rpc::{ + eth::types::EthBytes, state::StateCall, types::ApiTipsetKey, ApiPaths, Ctx, Permission, + RpcMethod, ServerError, + }, shim::{ address::{Address, Protocol}, clock::ChainEpoch, crypto::Signature, + message::Message, }, + state_manager::StateManager, utils::misc::env::is_env_set_and_truthy, }; +use crate::{ + rpc::eth::types::EthCallMessage, + shim::actors::{ + convert::{ + from_policy_v13_to_v10, from_policy_v13_to_v11, from_policy_v13_to_v12, + from_policy_v13_to_v14, from_policy_v13_to_v15, from_policy_v13_to_v16, + from_policy_v13_to_v9, + }, + miner, power, + }, +}; use ahash::{HashMap, HashSet}; use anyhow::Context as _; use fvm_ipld_blockstore::Blockstore; use jsonrpsee::core::{client::ClientT as _, params::ArrayParams}; use libp2p::PeerId; use num::Signed as _; +use once_cell::sync::Lazy; use once_cell::sync::OnceCell; use parking_lot::RwLock; use std::{borrow::Cow, fmt::Display, num::NonZeroU64, str::FromStr as _, sync::Arc}; @@ -567,6 +576,59 @@ impl RpcMethod<2> for SignMessage { } } +pub enum GetManifestFromContract {} + +impl GetManifestFromContract { + pub fn create_eth_call_message(contract: EthAddress) -> EthCallMessage { + // method ID of activationInformation(), + // see + static METHOD_ID: Lazy = + Lazy::new(|| EthBytes::from_str("0x2587660d").expect("Infallible")); + EthCallMessage { + to: Some(contract), + data: METHOD_ID.clone(), + ..Default::default() + } + } + + fn get_manifest_from_contract( + state_manager: &Arc>, + contract: EthAddress, + ) -> anyhow::Result { + let eth_call_message = Self::create_eth_call_message(contract); + let filecoin_message = Message::try_from(eth_call_message)?; + let api_invoc_result = StateCall::run(state_manager, &filecoin_message, None)?; + let Some(message_receipt) = api_invoc_result.msg_rct else { + anyhow::bail!("No message receipt"); + }; + message_receipt.try_into() + } +} + +impl RpcMethod<0> for GetManifestFromContract { + const NAME: &'static str = "F3.GetManifestFromContract"; + const PARAM_NAMES: [&'static str; 0] = []; + const API_PATHS: ApiPaths = ApiPaths::V1; + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = Some("Retrieves the manifest with all F3 parameters from a smart contract. The address of the contract is defined by the node."); + + type Params = (); + type Ok = Option; + + async fn handle( + ctx: Ctx, + _: Self::Params, + ) -> Result { + Ok(match ctx.chain_config().f3_contract_address() { + Some(f3_contract_address) => Some(Self::get_manifest_from_contract( + &ctx.state_manager, + f3_contract_address, + )?), + _ => None, + }) + } +} + /// returns a finality certificate at given instance number pub enum F3GetCertificate {} impl RpcMethod<1> for F3GetCertificate { diff --git a/src/rpc/methods/f3/contract_manifest_golden.json b/src/rpc/methods/f3/contract_manifest_golden.json new file mode 100644 index 000000000000..bc1c1085d6b0 --- /dev/null +++ b/src/rpc/methods/f3/contract_manifest_golden.json @@ -0,0 +1,58 @@ +{ + "Pause": false, + "ProtocolVersion": 5, + "InitialInstance": 0, + "BootstrapEpoch": 5000000, + "NetworkName": "filecoin", + "ExplicitPower": null, + "IgnoreECPower": false, + "InitialPowerTable": null, + "CommitteeLookback": 10, + "CatchUpAlignment": 15000000000, + "Gpbft": { + "Delta": 6000000000, + "DeltaBackOffExponent": 2.0, + "QualityDeltaMultiplier": 1.0, + "MaxLookaheadRounds": 5, + "ChainProposedLength": 100, + "RebroadcastBackoffBase": 6000000000, + "RebroadcastBackoffExponent": 1.3, + "RebroadcastBackoffSpread": 0.1, + "RebroadcastBackoffMax": 60000000000 + }, + "EC": { + "Period": 30000000000, + "Finality": 900, + "DelayMultiplier": 2.0, + "BaseDecisionBackoffTable": [ + 1.3, + 1.69, + 2.2, + 2.86, + 3.71, + 4.83, + 6.27, + 7.5 + ], + "HeadLookback": 0, + "Finalize": true + }, + "CertificateExchange": { + "ClientRequestTimeout": 10000000000, + "ServerRequestTimeout": 60000000000, + "MinimumPollInterval": 30000000000, + "MaximumPollInterval": 120000000000 + }, + "PubSub": { + "CompressionEnabled": false + }, + "ChainExchange": { + "SubscriptionBufferSize": 32, + "MaxChainLength": 100, + "MaxInstanceLookahead": 10, + "MaxDiscoveredChainsPerInstance": 1000, + "MaxWantedChainsPerInstance": 1000, + "RebroadcastInterval": 2000000000, + "MaxTimestampAge": 8000000000 + } +} \ No newline at end of file diff --git a/src/rpc/methods/f3/contract_return.hex b/src/rpc/methods/f3/contract_return.hex new file mode 100644 index 000000000000..57e37dde3f72 --- /dev/null +++ b/src/rpc/methods/f3/contract_return.hex @@ -0,0 +1 @@ +00000000000000000000000000000000000000000000000000000000004C4B400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000023D8554C172DA3014BCF3150CE78E079B06486FC1D0363321A590B6874E0FB2FC8C35C8922BC90D6926FFDE27D9C27620539F9EB52B7977DF939F07C3E168432A0DA30FC38C700DEFDC8A924652C9BF83D24C0AC4AEDCFAAD6086117E2BB42182DA3D63B7BE90D268A348B92A25CD2D7DEC1E07DE837994EA704F0ABB6194310E54323172E0EA58724699D9C84750088B8AF3FA537B2115AC620FB4E21A110E78200987EEB65816053306E04ECA4342E801C1B0D6111343F36FE50D677B5180301669747AA99FCA24B3EBCFF882AF4BE086E0EBB4C7F2C0024FFF9265E8408AFAB8A881BF568433F3E458EB8A1B861E9D89B021ACC9D1EA233990742B2B916A1FB1D5991326B003A5D490DE81D89BDC99F01FDF42A2244929D1C64A9059B620AE7F6732CF991DB161307993B62B152AB3ED0DC2374968A2F7D131125FEAA6C66D861B504CDAA326E3D7F23E32E17242F0BA1B2D79EA85E653B52E9740999DC846826FFF4FC718B69E6C39BDF67514446D399FFA7A12CC425FBF0FE6A7ADD3209AF97A165CB9EA5723E233E6D219ADBE95BF568B51159C9288411996314A0CAC8E3427620F6D34311A14660BBF2BD0E68115202B5377FA55503B507F409D11A767C43513ACA88A8DE478470DEE22FC62F2D8BA0BBC30BAD0CB4D95ECAAA4235A16381CDA3661256CFCA9BF9CAD693BC0E776F1184D152B8DED5F9565A076756293A895E5F65E187A84FC5FE774734E57BBC6974C53893941EA0ED138799D1F55D873FF83A0EDFFF13A23DF4929BA10A6ED099E509437CEF0BC17E3E065F00F000000 \ No newline at end of file diff --git a/src/rpc/methods/f3/types.rs b/src/rpc/methods/f3/types.rs index c323197e6ed3..5f9e7f906cf3 100644 --- a/src/rpc/methods/f3/types.rs +++ b/src/rpc/methods/f3/types.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0, MIT use super::*; -use crate::utils::multihash::prelude::*; use crate::{ blocks::{Tipset, TipsetKey}, lotus_json::{base64_standard, lotus_json_with_self, HasLotusJson, LotusJson}, networks::NetworkChain, + shim::executor::Receipt, + utils::multihash::prelude::*, }; +use byteorder::ByteOrder as _; use cid::Cid; use fil_actors_shared::fvm_ipld_bitfield::BitField; +use flate2::read::DeflateDecoder; use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; use fvm_shared4::ActorID; use itertools::Itertools as _; @@ -19,6 +22,7 @@ use once_cell::sync::Lazy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; +use std::io::Read as _; use std::{cmp::Ordering, time::Duration}; const MAX_LEASE_INSTANCES: u64 = 5; @@ -293,6 +297,116 @@ pub struct F3Manifest { } lotus_json_with_self!(F3Manifest); +impl F3Manifest { + pub fn get_eth_return_from_message_receipt(receipt: &Receipt) -> anyhow::Result> { + anyhow::ensure!( + receipt.exit_code().is_success(), + "unsuccessful exit code {}", + receipt.exit_code() + ); + let return_data = receipt.return_data(); + let eth_return = + fvm_ipld_encoding::from_slice::(&return_data)?.0; + Ok(eth_return) + } + + pub fn parse_contract_return(eth_return: &[u8]) -> anyhow::Result { + const INDEXING_SLICING_ERROR: &str = "unexpected overflow in indexing slicling"; + const MAX_PAYLOAD_LEN: usize = 4 << 10; + const SLOT_SIZE: usize = 32; + const SLOT_COUNT: usize = 3; + + // 3*32 because there should be 3 slots minimum + anyhow::ensure!( + eth_return.len() >= SLOT_COUNT * SLOT_SIZE, + "no activation information" + ); + let (slot_activation_epoch, slot_offset, slot_payload_len, payload) = ( + eth_return + .get(..SLOT_SIZE) + .context(INDEXING_SLICING_ERROR)?, + ð_return + .get(SLOT_SIZE..SLOT_SIZE * 2) + .context(INDEXING_SLICING_ERROR)?, + ð_return + .get(SLOT_SIZE * 2..SLOT_SIZE * 3) + .context(INDEXING_SLICING_ERROR)?, + ð_return + .get(SLOT_SIZE * 3..) + .context(INDEXING_SLICING_ERROR)?, + ); + // parse activation epoch from slot 1 + let activation_epoch = byteorder::BigEndian::read_u64( + slot_activation_epoch.last_chunk::<8>().expect("infallible"), + ); + // slot 2 is the offset to variable length bytes + // it is always the same 0x00000...0040 + for (i, &v) in slot_offset + .get(..SLOT_SIZE - 1) + .context(INDEXING_SLICING_ERROR)? + .iter() + .enumerate() + { + anyhow::ensure!( + v == 0, + "wrong value for offset (padding): slot[{i}] = 0x{v:x} != 0x00" + ); + } + let slot_offset_last = *slot_offset.last().context("unexpected empty slot_offset")?; + anyhow::ensure!( + slot_offset_last == 0x40, + "wrong value for offest : slot[31] = 0x{slot_offset_last:x} != 0x40", + ); + // parse payload length from slot 3 + let payload_len = + byteorder::BigEndian::read_u64(slot_payload_len.last_chunk::<8>().expect("infallible")) + as usize; + anyhow::ensure!( + payload_len <= MAX_PAYLOAD_LEN, + "too long declared payload: {payload_len} > {MAX_PAYLOAD_LEN}" + ); + anyhow::ensure!( + payload_len <= payload.len(), + "not enough remaining bytes: {payload_len} > {}", + payload.len() + ); + anyhow::ensure!( + activation_epoch < u64::MAX && payload_len > 0, + "no active activation" + ); + let compressed_manifest_bytes = payload + .get(..payload_len) + .context("not enough remaining bytes in payload")?; + let mut deflater = DeflateDecoder::new(compressed_manifest_bytes); + let mut manifest_bytes = vec![]; + deflater.read_to_end(&mut manifest_bytes)?; + let manifest: F3Manifest = serde_json::from_slice(&manifest_bytes)?; + anyhow::ensure!( + manifest.bootstrap_epoch >= 0 && manifest.bootstrap_epoch as u64 == activation_epoch, + "bootstrap epoch does not match: {} != {activation_epoch}", + manifest.bootstrap_epoch + ); + Ok(manifest) + } +} + +impl TryFrom<&Receipt> for F3Manifest { + type Error = anyhow::Error; + + fn try_from(receipt: &Receipt) -> Result { + let eth_return = Self::get_eth_return_from_message_receipt(receipt)?; + Self::parse_contract_return(ð_return) + } +} + +impl TryFrom for F3Manifest { + type Error = anyhow::Error; + + fn try_from(receipt: Receipt) -> Result { + Self::try_from(&receipt) + } +} + #[derive(PartialEq, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct SupplementalData { @@ -834,4 +948,19 @@ mod tests { let serialized = serde_json::to_value(cert.clone()).unwrap(); assert_eq!(lotus_json, serialized); } + + #[test] + fn test_f3_manifest_parse_contract_return() { + // The solidity contract: https://github.com/filecoin-project/f3-activation-contract/blob/063cd51a46f61b717375fe5675a6ddc73f4d8626/contracts/F3Parameters.sol + let eth_return_hex = include_str!("contract_return.hex").trim(); + let eth_return = hex::decode(eth_return_hex).unwrap(); + let manifest = F3Manifest::parse_contract_return(ð_return).unwrap(); + assert_eq!( + serde_json::to_value(&manifest).unwrap(), + serde_json::from_str::(include_str!( + "contract_manifest_golden.json" + )) + .unwrap(), + ); + } } diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 09d237ca027a..354b51cfff25 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -9,7 +9,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub use types::*; -use crate::blocks::Tipset; +use crate::blocks::{Tipset, TipsetKey}; use crate::chain::index::ResolveNullTipset; use crate::cid_collections::CidHashSet; use crate::eth::EthChainId; @@ -42,7 +42,7 @@ use crate::shim::{ state_tree::ActorState, version::NetworkVersion, }; use crate::state_manager::circulating_supply::GenesisInfo; -use crate::state_manager::{MarketBalance, StateOutput}; +use crate::state_manager::{MarketBalance, StateManager, StateOutput}; use crate::utils::db::{ car_stream::{CarBlock, CarWriter}, BlockstoreExt as _, @@ -77,6 +77,22 @@ const INITIAL_PLEDGE_NUM: u64 = 110; const INITIAL_PLEDGE_DEN: u64 = 100; pub enum StateCall {} + +impl StateCall { + pub fn run( + state_manager: &Arc>, + message: &Message, + tsk: Option, + ) -> anyhow::Result { + let tipset = state_manager + .chain_store() + .load_required_tipset_or_heaviest(&tsk)?; + // Handle expensive fork error? + // TODO(elmattic): https://github.com/ChainSafe/forest/issues/3733 + Ok(state_manager.call(message, Some(tipset))?) + } +} + impl RpcMethod<2> for StateCall { const NAME: &'static str = "Filecoin.StateCall"; const PARAM_NAMES: [&'static str; 2] = ["message", "tipsetKey"]; @@ -91,10 +107,7 @@ impl RpcMethod<2> for StateCall { ctx: Ctx, (message, ApiTipsetKey(tsk)): Self::Params, ) -> Result { - let tipset = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?; - // Handle expensive fork error? - // TODO(elmattic): https://github.com/ChainSafe/forest/issues/3733 - Ok(ctx.state_manager.call(&message, Some(tipset))?) + Ok(Self::run(&ctx.state_manager, &message, tsk)?) } } diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 237cbb616b5f..c6caa8614ff6 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -268,6 +268,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::f3::Finalize); $callback!($crate::rpc::f3::ProtectPeer); $callback!($crate::rpc::f3::SignMessage); + $callback!($crate::rpc::f3::GetManifestFromContract); // misc $callback!($crate::rpc::misc::GetActorEventsRaw); diff --git a/src/tool/subcommands/shed_cmd/f3.rs b/src/tool/subcommands/shed_cmd/f3.rs index cfee504c7390..3c020b36d3e6 100644 --- a/src/tool/subcommands/shed_cmd/f3.rs +++ b/src/tool/subcommands/shed_cmd/f3.rs @@ -1,21 +1,14 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -use crate::{ - rpc::{ - self, - eth::types::{EthAddress, EthBytes, EthCallMessage}, - f3::F3Manifest, - state::StateCall, - RpcMethodExt, - }, - shim::message::Message, +use crate::rpc::{ + self, + eth::types::EthAddress, + f3::{F3Manifest, GetManifestFromContract}, + state::StateCall, + RpcMethodExt, }; use clap::Subcommand; -use flate2::read::DeflateDecoder; -use std::{io::Read, str::FromStr as _}; - -const MAX_PAYLOAD_LEN: usize = 4 << 10; #[derive(Debug, Subcommand)] pub enum F3Commands { @@ -33,77 +26,21 @@ pub enum F3Commands { } impl F3Commands { - #[allow(clippy::indexing_slicing)] pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> { match self { Self::CheckActivation { contract: _ } => { unimplemented!("Will be done in a subsequent PR"); } Self::CheckActivationRaw { contract } => { - let eth_call_message = EthCallMessage { - to: Some(contract), - data: EthBytes::from_str("0x2587660d")?, // method ID of activationInformation() - ..Default::default() - }; - let filecoin_message = Message::try_from(eth_call_message)?; + let eth_call_message = GetManifestFromContract::create_eth_call_message(contract); let api_invoc_result = - StateCall::call(&client, (filecoin_message, None.into())).await?; + StateCall::call(&client, (eth_call_message.try_into()?, None.into())).await?; let Some(message_receipt) = api_invoc_result.msg_rct else { anyhow::bail!("No message receipt"); }; - anyhow::ensure!( - message_receipt.exit_code().is_success(), - "unsuccessful exit code {}", - message_receipt.exit_code() - ); - let return_data = message_receipt.return_data(); - let eth_return = - fvm_ipld_encoding::from_slice::(&return_data)?.0; + let eth_return = F3Manifest::get_eth_return_from_message_receipt(&message_receipt)?; println!("Raw data: {}", hex::encode(eth_return.as_slice())); - // 3*32 because there should be 3 slots minimum - anyhow::ensure!(eth_return.len() >= 3 * 32, "no activation information"); - let mut activation_epoch_bytes: [u8; 8] = [0; 8]; - activation_epoch_bytes.copy_from_slice(ð_return[24..32]); - let activation_epoch = u64::from_be_bytes(activation_epoch_bytes); - for (i, &v) in eth_return[32..63].iter().enumerate() { - anyhow::ensure!( - v == 0, - "wrong value for offset (padding): slot[{i}] = 0x{v:x} != 0x00" - ); - } - anyhow::ensure!( - eth_return[63] == 0x40, - "wrong value for offest : slot[31] = 0x{:x} != 0x40", - eth_return[63] - ); - let mut payload_len_bytes: [u8; 8] = [0; 8]; - payload_len_bytes.copy_from_slice(ð_return[88..96]); - let payload_len = u64::from_be_bytes(payload_len_bytes) as usize; - anyhow::ensure!( - payload_len <= MAX_PAYLOAD_LEN, - "too long declared payload: {payload_len} > {MAX_PAYLOAD_LEN}" - ); - let payload_bytes = ð_return[96..]; - anyhow::ensure!( - payload_len <= payload_bytes.len(), - "not enough remaining bytes: {payload_len} > {}", - payload_bytes.len() - ); - anyhow::ensure!( - activation_epoch < u64::MAX && payload_len > 0, - "no active activation" - ); - let compressed_manifest_bytes = &payload_bytes[..payload_len]; - let mut deflater = DeflateDecoder::new(compressed_manifest_bytes); - let mut manifest_bytes = vec![]; - deflater.read_to_end(&mut manifest_bytes)?; - let manifest: F3Manifest = serde_json::from_slice(&manifest_bytes)?; - anyhow::ensure!( - manifest.bootstrap_epoch >= 0 - && manifest.bootstrap_epoch as u64 == activation_epoch, - "bootstrap epoch does not match: {} != {activation_epoch}", - manifest.bootstrap_epoch - ); + let manifest = F3Manifest::parse_contract_return(ð_return)?; println!("{}", serde_json::to_string_pretty(&manifest)?); } }