diff --git a/Cargo.lock b/Cargo.lock index 57e186d6f5..6e283c664b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -287,15 +287,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object 0.32.2", -] - [[package]] name = "arbitrary" version = "1.4.2" @@ -314,6 +305,15 @@ dependencies = [ "arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" +dependencies = [ + "rustversion", +] + [[package]] name = "argon2" version = "0.5.3" @@ -1240,7 +1240,7 @@ dependencies = [ "serde_urlencoded", "thiserror 1.0.69", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "url", "winapi", ] @@ -1268,9 +1268,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", "serde", @@ -1421,9 +1421,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -1572,9 +1572,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" dependencies = [ "clap_builder", "clap_derive", @@ -1582,9 +1582,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" dependencies = [ "anstream", "anstyle", @@ -1594,11 +1594,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.60" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" +checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" dependencies = [ - "clap 4.5.51", + "clap 4.5.50", ] [[package]] @@ -2346,9 +2346,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", "serde_core", @@ -3137,7 +3137,7 @@ checksum = "3a82608ee96ce76aeab659e9b8d3c2b787bffd223199af88c674923d861ada10" dependencies = [ "execute-command-macro", "execute-command-tokens", - "generic-array 1.3.5", + "generic-array 1.3.4", ] [[package]] @@ -3380,6 +3380,7 @@ dependencies = [ "fendermint_vm_snapshot", "fendermint_vm_topdown", "fendermint_vm_topdown_proof_service", + "filecoin-f3-gpbft", "fs-err", "fvm", "fvm_ipld_blockstore 0.3.1", @@ -3417,7 +3418,7 @@ dependencies = [ "tendermint-proto 0.31.1", "tendermint-rpc", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "toml 0.8.23", "tower 0.4.13", "tower-abci", @@ -3434,7 +3435,7 @@ dependencies = [ "anyhow", "bytes", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "ethers", "fendermint_materializer", "fendermint_vm_actor_interface", @@ -3462,6 +3463,7 @@ dependencies = [ "dirs", "fendermint_vm_encoding", "fendermint_vm_topdown", + "fendermint_vm_topdown_proof_service", "fvm_ipld_encoding 0.5.3", "fvm_shared", "ipc-api", @@ -3537,7 +3539,7 @@ dependencies = [ "async-trait", "axum", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "erased-serde", "ethers", "ethers-contract", @@ -3658,7 +3660,7 @@ dependencies = [ "tendermint-rpc", "text-tables", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "toml 0.8.23", "tracing", "url", @@ -3690,7 +3692,7 @@ dependencies = [ "base64 0.21.7", "bytes", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "ethers", "fendermint_crypto", "fendermint_vm_actor_interface", @@ -3868,10 +3870,12 @@ dependencies = [ name = "fendermint_vm_interpreter" version = "0.1.0" dependencies = [ + "actors-builtin-car", "actors-custom-api", "actors-custom-car", "anyhow", "arbitrary", + "arc-swap", "async-stm", "async-trait", "base64 0.21.7", @@ -3897,6 +3901,7 @@ dependencies = [ "fendermint_vm_message", "fendermint_vm_resolver", "fendermint_vm_topdown", + "fendermint_vm_topdown_proof_service", "fil_actor_eam", "fil_actor_evm", "futures-core", @@ -3917,6 +3922,7 @@ dependencies = [ "num-traits", "pin-project", "prometheus", + "proofs", "quickcheck", "quickcheck_macros", "rand 0.8.5", @@ -3931,7 +3937,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tracing", ] @@ -3950,6 +3956,7 @@ dependencies = [ "fendermint_vm_actor_interface", "fendermint_vm_encoding", "fendermint_vm_message", + "fendermint_vm_topdown_proof_service", "fvm_ipld_encoding 0.5.3", "fvm_shared", "hex", @@ -3957,6 +3964,7 @@ dependencies = [ "lazy_static", "multihash-codetable", "num-traits", + "proofs", "quickcheck", "quickcheck_macros", "rand 0.8.5", @@ -4017,7 +4025,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tracing", "unsigned-varint 0.7.2", ] @@ -4032,7 +4040,7 @@ dependencies = [ "async-trait", "bytes", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "ethers", "fendermint_crypto", "fendermint_testing", @@ -4070,7 +4078,7 @@ dependencies = [ "base64 0.21.7", "chrono", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "fendermint_actor_f3_light_client", "fendermint_vm_genesis", "filecoin-f3-certs", @@ -4084,7 +4092,6 @@ dependencies = [ "humantime-serde", "ipc-api", "ipc-observability", - "ipc-provider", "keccak-hash", "multihash 0.18.1", "multihash-codetable", @@ -4139,7 +4146,7 @@ dependencies = [ "anyhow", "async-std", "cid 0.10.1", - "clap 4.5.51", + "clap 4.5.50", "futures", "fvm_ipld_blockstore 0.2.1", "fvm_ipld_car 0.7.1", @@ -4161,7 +4168,7 @@ dependencies = [ "fvm_ipld_blockstore 0.3.1", "fvm_ipld_encoding 0.5.3", "fvm_shared", - "hex-literal 1.1.0", + "hex-literal 1.0.0", "log", "multihash 0.19.3", "num-derive 0.4.2", @@ -4186,7 +4193,7 @@ dependencies = [ "fvm_ipld_kamt", "fvm_shared", "hex", - "hex-literal 1.1.0", + "hex-literal 1.0.0", "log", "multihash-codetable", "num-derive 0.4.2", @@ -4227,7 +4234,7 @@ dependencies = [ "fvm_sdk", "fvm_shared", "hex", - "integer-encoding 4.1.0", + "integer-encoding 4.0.2", "itertools 0.14.0", "k256 0.13.4", "lazy_static", @@ -4251,7 +4258,7 @@ dependencies = [ [[package]] name = "filecoin-f3-blssig" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "blake2 0.11.0-rc.2", "bls-signatures", @@ -4267,7 +4274,7 @@ dependencies = [ [[package]] name = "filecoin-f3-certs" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "ahash 0.8.12", "filecoin-f3-gpbft", @@ -4278,7 +4285,7 @@ dependencies = [ [[package]] name = "filecoin-f3-gpbft" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "ahash 0.8.12", "anyhow", @@ -4301,7 +4308,7 @@ dependencies = [ [[package]] name = "filecoin-f3-lightclient" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "anyhow", "base64 0.22.1", @@ -4317,7 +4324,7 @@ dependencies = [ [[package]] name = "filecoin-f3-merkle" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "anyhow", "sha3", @@ -4326,7 +4333,7 @@ dependencies = [ [[package]] name = "filecoin-f3-rpc" version = "0.1.0" -source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#40af605984045a9f2b9ba5dcc9c04c984deb8d1f" dependencies = [ "anyhow", "filecoin-f3-gpbft", @@ -4443,9 +4450,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide 0.8.9", @@ -4951,7 +4958,7 @@ dependencies = [ "serde", "serde_ipld_dagcbor 0.6.4", "serde_repr", - "serde_tuple 1.1.3", + "serde_tuple 1.1.2", "thiserror 2.0.17", ] @@ -5064,9 +5071,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.3.5" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +checksum = "985a5578ebdb02351d484a77fb27e7cb79272f1ba9bc24692d8243c3cfe40660" dependencies = [ "rustversion", "typenum", @@ -5208,7 +5215,7 @@ dependencies = [ "indexmap 2.12.0", "slab", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tracing", ] @@ -5227,7 +5234,7 @@ dependencies = [ "indexmap 2.12.0", "slab", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tracing", ] @@ -5375,9 +5382,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex-literal" -version = "1.1.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" [[package]] name = "hex_fmt" @@ -5472,11 +5479,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.12" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5690,7 +5697,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.3", ] [[package]] @@ -5787,9 +5794,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", @@ -5800,9 +5807,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -5813,10 +5820,11 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ + "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -5827,38 +5835,42 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", + "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", "icu_locale_core", + "stable_deref_trait", + "tinystr", "writeable", "yoke", "zerofrom", @@ -5948,9 +5960,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" dependencies = [ "crossbeam-deque", "globset", @@ -6098,9 +6110,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integer-encoding" -version = "4.1.0" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" +checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" [[package]] name = "io-lifetimes" @@ -6164,7 +6176,7 @@ dependencies = [ "bytes", "chrono", "cid 0.11.1", - "clap 4.5.51", + "clap 4.5.50", "clap_complete", "contracts-artifacts", "env_logger 0.10.2", @@ -6219,7 +6231,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-tungstenite 0.18.0", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "toml 0.7.8", "tracing", "tracing-subscriber 0.3.20", @@ -6343,7 +6355,7 @@ dependencies = [ "ethers", "fs-err", "fvm_shared", - "generic-array 1.3.5", + "generic-array 1.3.4", "hex", "ipc-types", "libc", @@ -6460,13 +6472,13 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.17" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6578,9 +6590,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -6642,7 +6654,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tokio-rustls 0.26.4", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tracing", "url", ] @@ -7546,9 +7558,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "literally" @@ -8853,9 +8865,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -8980,9 +8992,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8e0f6df8eaa422d97d72edcd152e1451618fed47fabbdbd5a8864167b1d4aff7" dependencies = [ "unicode-ident", ] @@ -9099,11 +9111,12 @@ dependencies = [ [[package]] name = "proptest" -version = "1.9.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" dependencies = [ "bitflags 2.10.0", + "lazy_static", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -9205,11 +9218,10 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psm" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" dependencies = [ - "ar_archive_writer", "cc", ] @@ -9683,7 +9695,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.26.4", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tower 0.5.2", "tower-http 0.6.6", "tower-service", @@ -9692,7 +9704,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.3", ] [[package]] @@ -10005,7 +10017,7 @@ dependencies = [ "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -10057,9 +10069,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", "zeroize", @@ -10079,7 +10091,7 @@ dependencies = [ "rustls 0.23.34", "rustls-native-certs 0.8.2", "rustls-platform-verifier-android", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.7", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", @@ -10104,9 +10116,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -10219,9 +10231,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "dyn-clone", "ref-cast", @@ -10513,12 +10525,12 @@ dependencies = [ [[package]] name = "serde_tuple" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af196b9c06f0aa5555ab980c01a2527b0f67517da8d68b1731b9d4764846a6f" +checksum = "52569c5296679bd28e2457f067f97d270077df67da0340647da5412c8eac8d9e" dependencies = [ "serde", - "serde_tuple_macros 1.1.3", + "serde_tuple_macros 1.1.2", ] [[package]] @@ -10534,9 +10546,9 @@ dependencies = [ [[package]] name = "serde_tuple_macros" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3a1e7d2eadec84deabd46ae061bf480a91a6bce74d25dad375bd656f2e19d8" +checksum = "2f46c707781471741d5f2670edb36476479b26e94cf43efe21ca3c220b97ef2e" dependencies = [ "proc-macro2", "quote", @@ -10583,7 +10595,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.5", + "schemars 1.0.4", "serde_core", "serde_json", "time", @@ -11384,9 +11396,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "target-triple" -version = "1.0.0" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" [[package]] name = "tempfile" @@ -11708,9 +11720,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -11879,9 +11891,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -12032,7 +12044,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tower-layer", "tower-service", "tracing", @@ -12267,9 +12279,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.113" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559b6a626c0815c942ac98d434746138b4f89ddd6a1b8cbb168c6845fb3376c5" +checksum = "4d66678374d835fe847e0dc8348fde2ceb5be4a7ec204437d8367f0d8df266a5" dependencies = [ "glob", "serde", @@ -12391,9 +12403,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" [[package]] name = "unicode-segmentation" @@ -12620,7 +12632,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-tungstenite 0.21.0", - "tokio-util 0.7.17", + "tokio-util 0.7.16", "tower-service", "tracing", ] @@ -12642,9 +12654,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", @@ -12653,11 +12665,25 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.108", + "wasm-bindgen-shared", +] + [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -12668,9 +12694,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12678,22 +12704,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ - "bumpalo", "proc-macro2", "quote", "syn 2.0.108", + "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -12941,9 +12967,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -12985,14 +13011,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.4", + "webpki-root-certs 1.0.3", ] [[package]] name = "webpki-root-certs" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" dependencies = [ "rustls-pki-types", ] @@ -13014,9 +13040,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -13554,9 +13580,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "ws_stream_wasm" @@ -13627,9 +13653,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmltree" @@ -13725,10 +13751,11 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ + "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -13736,9 +13763,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -13809,9 +13836,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", "yoke", @@ -13820,9 +13847,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -13831,9 +13858,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c5156b3619..12a93b364e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ members = [ "fendermint/testing/*-test", "fendermint/tracing", "fendermint/vm/*", - "fendermint/vm/topdown/proof-service", "fendermint/actors", "fendermint/actors-custom-car", "fendermint/actors-builtin-car", @@ -95,6 +94,7 @@ gcra = "0.6.0" hex = "0.4" hex-literal = "0.4.1" http = "0.2.12" +humantime-serde = "1.1" im = "15.1.0" integer-encoding = { version = "3.0.3", default-features = false } jsonrpc-v2 = { version = "0.11", default-features = false, features = [ @@ -137,6 +137,7 @@ num-bigint = "0.4" num-derive = "0.4" num-traits = "0.2" num_enum = "0.7.2" +parking_lot = "0.12" paste = "1" pin-project = "1.1.2" prometheus = { version = "0.13", features = ["process"] } @@ -185,8 +186,6 @@ tracing-appender = "0.2.3" text-tables = "0.3.1" url = { version = "2.4.1", features = ["serde"] } zeroize = "1.6" -parking_lot = "0.12" -humantime-serde = "1.1" # Vendored for cross-compilation, see https://github.com/cross-rs/cross/wiki/Recipes#openssl # Make sure every top level build target actually imports this dependency, and don't end up diff --git a/fendermint/actors/f3-light-client/src/lib.rs b/fendermint/actors/f3-light-client/src/lib.rs index 0898c8243c..f0ea8b97e9 100644 --- a/fendermint/actors/f3-light-client/src/lib.rs +++ b/fendermint/actors/f3-light-client/src/lib.rs @@ -40,9 +40,9 @@ impl F3LightClientActor { rt.validate_immediate_caller_is(std::iter::once(&SYSTEM_ACTOR_ADDR))?; let state = State::new( - params.instance_id, + params.latest_instance_id, + params.latest_finalized_height, params.power_table, - params.finalized_epochs, )?; rt.create(&state)?; @@ -70,8 +70,8 @@ impl F3LightClient for F3LightClientActor { let lc = &state.light_client_state; Ok(GetStateResponse { - instance_id: lc.instance_id, - finalized_epochs: lc.finalized_epochs.clone(), + latest_instance_id: lc.latest_instance_id, + latest_finalized_height: lc.latest_finalized_height, power_table: lc.power_table.clone(), }) } @@ -99,17 +99,18 @@ mod tests { use fil_actors_runtime::SYSTEM_ACTOR_ADDR; use fvm_ipld_encoding::ipld_block::IpldBlock; use fvm_shared::address::Address; + use fvm_shared::clock::ChainEpoch; use fvm_shared::error::ExitCode; /// Helper function to create test light client state fn create_test_state( - instance_id: u64, - finalized_epochs: Vec, + current_instance_id: u64, + latest_finalized_epoch: Option, power_table: Vec, ) -> LightClientState { LightClientState { - instance_id, - finalized_epochs, + latest_instance_id: current_instance_id, + latest_finalized_height: latest_finalized_epoch, power_table, } } @@ -118,10 +119,12 @@ mod tests { fn create_test_power_entries() -> Vec { vec![ PowerEntry { + id: 1, public_key: vec![1, 2, 3], power: 100, }, PowerEntry { + id: 2, public_key: vec![4, 5, 6], power: 200, }, @@ -130,9 +133,9 @@ mod tests { /// Construct the actor and verify initialization pub fn construct_and_verify( - instance_id: u64, + current_instance_id: u64, power_table: Vec, - finalized_epochs: Vec, + latest_finalized_epoch: Option, ) -> MockRuntime { let rt = MockRuntime { receiver: Address::new_id(10), @@ -144,9 +147,9 @@ mod tests { rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); let constructor_params = ConstructorParams { - instance_id, + latest_instance_id: current_instance_id, + latest_finalized_height: latest_finalized_epoch, power_table, - finalized_epochs, }; let result = rt @@ -165,33 +168,26 @@ mod tests { #[test] fn test_constructor_empty_power_table() { - let _rt = construct_and_verify(0, vec![], vec![]); + let _rt = construct_and_verify(0, vec![], Some(10)); // Constructor test passed if we get here without panicking } #[test] fn test_constructor_with_power_table() { let power_entries = create_test_power_entries(); - let _rt = construct_and_verify(1, power_entries, vec![]); - // Constructor test passed if we get here without panicking - } - - #[test] - fn test_constructor_with_finalized_epochs() { - let power_entries = create_test_power_entries(); - let _rt = construct_and_verify(1, power_entries, vec![100, 101, 102]); + let _rt = construct_and_verify(1, power_entries, Some(10)); // Constructor test passed if we get here without panicking } #[test] fn test_update_state_success() { - let rt = construct_and_verify(1, create_test_power_entries(), vec![]); + let rt = construct_and_verify(1, create_test_power_entries(), Some(10)); // Set caller to system actor rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - let new_state = create_test_state(1, vec![100, 101, 102], create_test_power_entries()); + let new_state = create_test_state(1, Some(10), create_test_power_entries()); let update_params = UpdateStateParams { state: new_state.clone(), }; @@ -207,16 +203,53 @@ mod tests { rt.verify(); } + #[test] + fn test_update_state_non_advancing_height() { + let rt = construct_and_verify(1, create_test_power_entries(), Some(10)); + + // First update to set the finalized height to 102 + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let initial_state = create_test_state(1, Some(10), create_test_power_entries()); + let initial_params = UpdateStateParams { + state: initial_state, + }; + rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&initial_params).unwrap(), + ) + .unwrap(); + rt.reset(); + + // Try to update with same height + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let same_height_state = create_test_state(1, Some(10), create_test_power_entries()); + let update_params = UpdateStateParams { + state: same_height_state, + }; + + let result = rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&update_params).unwrap(), + ); + + // Should fail with illegal argument + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT); + } + #[test] fn test_update_state_unauthorized_caller() { - let rt = construct_and_verify(1, create_test_power_entries(), vec![]); + let rt = construct_and_verify(1, create_test_power_entries(), Some(10)); // Set caller to non-system actor let unauthorized_caller = Address::new_id(999); rt.set_caller(*SYSTEM_ACTOR_CODE_ID, unauthorized_caller); rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - let new_state = create_test_state(1, vec![100, 101, 102], create_test_power_entries()); + let new_state = create_test_state(1, Some(11), create_test_power_entries()); let update_params = UpdateStateParams { state: new_state }; let result = rt.call::( @@ -233,12 +266,12 @@ mod tests { #[test] fn test_get_state() { let power_entries = create_test_power_entries(); - let rt = construct_and_verify(42, power_entries.clone(), vec![]); + let rt = construct_and_verify(42, power_entries.clone(), Some(10)); // Update state first rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - let new_state = create_test_state(42, vec![100, 101, 102], power_entries.clone()); + let new_state = create_test_state(42, Some(11), power_entries.clone()); let update_params = UpdateStateParams { state: new_state }; rt.call::( Method::UpdateState as u64, @@ -255,19 +288,19 @@ mod tests { .unwrap(); let response = result.deserialize::().unwrap(); - assert_eq!(response.instance_id, 42); - assert_eq!(response.finalized_epochs, vec![100, 101, 102]); + assert_eq!(response.latest_instance_id, 42); + assert_eq!(response.latest_finalized_height, Some(11)); assert_eq!(response.power_table, power_entries); } #[test] fn test_state_progression() { - let rt = construct_and_verify(1, create_test_power_entries(), vec![]); + let rt = construct_and_verify(1, create_test_power_entries(), Some(10)); // Update with first state rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - let state1 = create_test_state(1, vec![100, 101, 102], create_test_power_entries()); + let state1 = create_test_state(1, Some(100), create_test_power_entries()); let params1 = UpdateStateParams { state: state1 }; rt.call::( Method::UpdateState as u64, @@ -279,7 +312,7 @@ mod tests { // Update with second state (higher height) rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); - let state2 = create_test_state(1, vec![200, 201, 202], create_test_power_entries()); + let state2 = create_test_state(1, Some(200), create_test_power_entries()); let params2 = UpdateStateParams { state: state2 }; let result = rt.call::( Method::UpdateState as u64, @@ -287,4 +320,94 @@ mod tests { ); assert!(result.is_ok()); } + + #[test] + fn test_instance_id_progression_next_instance() { + let rt = construct_and_verify(100, create_test_power_entries(), Some(10)); + + // First state at instance 100 + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let initial_state = create_test_state(100, Some(10), create_test_power_entries()); + let initial_params = UpdateStateParams { + state: initial_state, + }; + rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&initial_params).unwrap(), + ) + .unwrap(); + rt.reset(); + + // Update to next instance (100 -> 101) should succeed + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let next_instance_state = create_test_state(101, Some(10), create_test_power_entries()); + let update_params = UpdateStateParams { + state: next_instance_state, + }; + + let result = rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&update_params).unwrap(), + ); + assert!(result.is_ok()); + } + + #[test] + fn test_instance_id_skip_rejected() { + let rt = construct_and_verify(100, create_test_power_entries(), Some(10)); + + // First state at instance 100 + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let initial_state = create_test_state(100, Some(10), create_test_power_entries()); + let initial_params = UpdateStateParams { + state: initial_state, + }; + rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&initial_params).unwrap(), + ) + .unwrap(); + rt.reset(); + + // Try to skip instance (100 -> 102) should fail + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + let skipped_state = create_test_state(102, Some(10), create_test_power_entries()); + let update_params = UpdateStateParams { + state: skipped_state, + }; + + let result = rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&update_params).unwrap(), + ); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT); + } + + #[test] + fn test_empty_epochs_rejected() { + let rt = construct_and_verify(1, create_test_power_entries(), Some(10)); + + rt.set_caller(*SYSTEM_ACTOR_CODE_ID, SYSTEM_ACTOR_ADDR); + rt.expect_validate_caller_addr(vec![SYSTEM_ACTOR_ADDR]); + + // Try to update with empty finalized_epochs + let invalid_state = create_test_state(1, Some(10), create_test_power_entries()); + let update_params = UpdateStateParams { + state: invalid_state, + }; + + let result = rt.call::( + Method::UpdateState as u64, + IpldBlock::serialize_cbor(&update_params).unwrap(), + ); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.exit_code(), ExitCode::USR_ILLEGAL_ARGUMENT); + } } diff --git a/fendermint/actors/f3-light-client/src/state.rs b/fendermint/actors/f3-light-client/src/state.rs index 64497e9d5c..1142626f5c 100644 --- a/fendermint/actors/f3-light-client/src/state.rs +++ b/fendermint/actors/f3-light-client/src/state.rs @@ -9,6 +9,7 @@ use crate::types::{LightClientState, PowerEntry}; use fil_actors_runtime::runtime::Runtime; use fil_actors_runtime::ActorError; +use fvm_shared::clock::ChainEpoch; use serde::{Deserialize, Serialize}; /// State of the F3 light client actor. @@ -25,14 +26,14 @@ pub struct State { impl State { /// Create a new F3 light client state pub fn new( - instance_id: u64, + latest_instance_id: u64, + latest_finalized_height: Option, power_table: Vec, - finalized_epochs: Vec, ) -> Result { let state = State { light_client_state: LightClientState { - instance_id, - finalized_epochs, + latest_instance_id, + latest_finalized_height, power_table, }, }; @@ -40,10 +41,6 @@ impl State { } /// Update light client state - /// - /// This method should only be called from consensus code path which - /// contains the lightclient verifier. No additional validation is - /// performed here as it's expected to be done by the verifier. pub fn update_state( &mut self, _rt: &impl Runtime, diff --git a/fendermint/actors/f3-light-client/src/types.rs b/fendermint/actors/f3-light-client/src/types.rs index 7065f700d4..ffe644e777 100644 --- a/fendermint/actors/f3-light-client/src/types.rs +++ b/fendermint/actors/f3-light-client/src/types.rs @@ -12,28 +12,27 @@ use fvm_shared::clock::ChainEpoch; /// F3 Light Client State - maintains verifiable parent finality from the parent chain. /// /// This structure represents the essential state needed to track F3 finality: -/// - Instance ID: The current F3 instance (can increment during protocol upgrades) -/// - Finalized Epochs: Complete chain of finalized epochs (not just the latest) +/// - Latest Instance ID: The latest F3 instance that has been committed +/// - Latest Finalized Height: The highest epoch that has been finalized (None if nothing finalized yet) /// - Power Table: Current validator power table (can change between instances) /// /// This state is extracted from F3 certificates received from the parent chain /// and stored by the actor for use in finality proofs. #[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq)] pub struct LightClientState { - /// Current F3 instance ID - pub instance_id: u64, - /// Finalized chain - full list of finalized epochs - /// Matches ECChain from F3 certificates - /// Empty initially at genesis until first update - pub finalized_epochs: Vec, + /// Latest F3 instance ID that has been committed + pub latest_instance_id: u64, + /// The latest finalized height (None if nothing has been finalized yet) + pub latest_finalized_height: Option, /// Current power table for this instance - /// Power table can change between instances pub power_table: Vec, } /// Power table entry for F3 consensus #[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq)] pub struct PowerEntry { + /// Validator ID (from F3 power table) + pub id: u64, /// Public key of the validator pub public_key: Vec, /// Voting power of the validator @@ -44,11 +43,11 @@ pub struct PowerEntry { #[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq)] pub struct ConstructorParams { /// Initial F3 instance ID (from genesis) - pub instance_id: u64, + pub latest_instance_id: u64, /// Initial power table (from genesis) pub power_table: Vec, - /// Initial finalized epochs (from genesis certificate) - pub finalized_epochs: Vec, + /// Initial finalized height (None if nothing finalized yet, Some(height) from genesis certificate if available) + pub latest_finalized_height: Option, } /// Parameters for updating the light client state @@ -61,10 +60,10 @@ pub struct UpdateStateParams { /// Response containing the current light client state #[derive(Deserialize_tuple, Serialize_tuple, Debug, Clone, PartialEq, Eq)] pub struct GetStateResponse { - /// Current F3 instance ID - pub instance_id: u64, - /// Finalized chain - full list of finalized epochs (ordered) - pub finalized_epochs: Vec, + /// Latest F3 instance ID that has been committed + pub latest_instance_id: u64, + /// The latest finalized height (None if nothing finalized yet) + pub latest_finalized_height: Option, /// Current power table pub power_table: Vec, } diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index 01c8a95803..7f43973dad 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -45,8 +45,10 @@ tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } literally = { workspace = true } url = { workspace = true } +ethers = { workspace = true } fendermint_abci = { path = "../abci" } +ipc_actors_abis = { path = "../../contract-bindings" } actors-custom-api = { path = "../actors/api" } fendermint_actor_f3_light_client = { path = "../actors/f3-light-client" } fendermint_app_options = { path = "./options" } @@ -73,8 +75,8 @@ fendermint_vm_snapshot = { path = "../vm/snapshot" } fendermint_vm_topdown = { path = "../vm/topdown" } fendermint_vm_topdown_proof_service = { path = "../vm/topdown/proof-service" } -ipc_actors_abis = { path = "../../contract-bindings" } -ethers = {workspace = true} +# F3 certificate handling +filecoin-f3-gpbft = { git = "https://github.com/moshababo/rust-f3", branch = "cargo-git-compat" } # .car file wrapped in a crate actors-builtin-car = { path = "../actors-builtin-car" } diff --git a/fendermint/app/config/default.toml b/fendermint/app/config/default.toml index 1aa0174248..b965e93ac7 100644 --- a/fendermint/app/config/default.toml +++ b/fendermint/app/config/default.toml @@ -273,6 +273,51 @@ vote_interval = 1 # pausing the syncer, preventing new events to trigger votes. vote_timeout = 60 +# # Top-down checkpoint configuration (uncomment to enable parent syncing) +# [ipc.topdown] +# # Number of blocks to delay before considering a parent block final +# chain_head_delay = 10 +# # Additional delay on top of chain_head_delay before proposing finality +# proposal_delay = 5 +# # Maximum number of blocks to propose in a single checkpoint +# max_proposal_range = 100 +# # Maximum number of blocks to cache (optional) +# # max_cache_blocks = 1000 +# # Parent syncing cron period, in seconds +# polling_interval = 30 +# # Exponential backoff retry base, in seconds +# exponential_back_off = 5 +# # Maximum number of retries before giving up +# exponential_retry_limit = 5 +# # Parent HTTP RPC endpoint +# parent_http_endpoint = "http://api.calibration.node.glif.io/rpc/v1" +# # Parent HTTP timeout (optional), in seconds +# # parent_http_timeout = 60 +# # Bearer token for Authorization header (optional) +# # parent_http_auth_token = "your-token-here" +# # Parent registry address +# parent_registry = "0x74539671a1d2f1c8f200826baba665179f53a1b7" +# # Parent gateway address +# parent_gateway = "0x77aa40b105843728088c0132e43fc44348881da8" +# +# # F3 proof service configuration (optional - for proof-based parent finality) +# # Requires genesis to have F3 parameters configured +# [ipc.topdown.proof_service] +# # Enable F3 proof-based parent finality (default: false) +# enabled = false +# # F3 network name - must match parent chain ("calibrationnet", "mainnet") +# f3_network_name = "calibrationnet" +# # How often to poll parent chain for new F3 certificates, in seconds +# polling_interval = 30 +# # How many F3 instances ahead to pre-generate proofs (lookahead window) +# lookahead_instances = 5 +# # How many old instances to keep after commitment (retention window) +# retention_instances = 2 +# # Gateway actor ID on parent chain (optional - derived from genesis if not set) +# # gateway_actor_id = 176609 +# # Or use Ethereum address (will be resolved to actor ID) +# # gateway_eth_address = "0xE4c61299c16323C4B58376b60A77F68Aa59afC8b" + # # Setting which are only allowed if the `--network` CLI parameter is `testnet`. # [testing] diff --git a/fendermint/app/settings/Cargo.toml b/fendermint/app/settings/Cargo.toml index 20aaeee513..db90508c24 100644 --- a/fendermint/app/settings/Cargo.toml +++ b/fendermint/app/settings/Cargo.toml @@ -32,3 +32,4 @@ ipc-observability = { path = "../../../ipc/observability" } fendermint_vm_encoding = { path = "../../vm/encoding" } fendermint_vm_topdown = { path = "../../vm/topdown" } +fendermint_vm_topdown_proof_service = { path = "../../vm/topdown/proof-service" } diff --git a/fendermint/app/settings/src/lib.rs b/fendermint/app/settings/src/lib.rs index ab738dfa75..9a456e54f4 100644 --- a/fendermint/app/settings/src/lib.rs +++ b/fendermint/app/settings/src/lib.rs @@ -27,6 +27,8 @@ use self::resolver::ResolverSettings; use ipc_observability::config::TracingSettings; use ipc_provider::config::deserialize::deserialize_eth_address_from_str; +use fendermint_vm_topdown_proof_service::ProofServiceConfig; + pub mod eth; pub mod fvm; pub mod resolver; @@ -226,6 +228,18 @@ pub struct TopDownSettings { /// The parent gateway address #[serde(deserialize_with = "deserialize_eth_address_from_str")] pub parent_gateway: Address, + /// F3 configuration (optional - for proof-based finality) + /// If Some, F3 proof-based finality is enabled; if None, use legacy voting-based finality + #[serde(default)] + pub f3: Option, +} + +/// F3 proof-based finality configuration +/// When present, F3 proof-based finality is enabled +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct F3 { + /// F3 proof service configuration (mandatory when F3 is configured) + pub proof_service: ProofServiceConfig, } #[serde_as] diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 747f79b130..73954fc22f 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -23,8 +23,8 @@ use fendermint_storage::{ }; use fendermint_vm_core::Timestamp; use fendermint_vm_interpreter::fvm::state::{ - empty_state_tree, CheckStateRef, FvmExecState, FvmQueryState, FvmStateParams, - FvmUpdatableParams, + empty_state_tree, ipc::F3LightClientCaller, CheckStateRef, FvmExecState, FvmQueryState, + FvmStateParams, FvmUpdatableParams, }; use fendermint_vm_interpreter::fvm::store::ReadOnlyBlockstore; use fendermint_vm_interpreter::genesis::{read_genesis_car, GenesisAppState}; @@ -218,6 +218,77 @@ where } } +/// Create a read-only execution state for querying actors before app initialization. +/// +/// This allows querying state (e.g., F3 Light Client) before the full App is created. +/// Returns `None` if the state hasn't been initialized by genesis yet. +pub fn create_read_only_exec_state( + db: &DB, + state_store: &BS, + namespace: KV::Namespace, +) -> Result>>>> +where + KV: KVStore + Codec + Encode, + DB: KVReadable + 'static, + BS: Blockstore + Clone + 'static + Send + Sync, +{ + // Read committed state from database (same pattern as get_committed_state) + let tx = db.read(); + let state: Option = tx + .get(&namespace, &AppStoreKey::State) + .context("get failed")?; + + let state = match state { + Some(s) => s, + None => return Ok(None), + }; + + let block_height = state.app_state.block_height; + let state_params = state.app_state.state_params; + + // Check if state is queryable (genesis has been initialized) + // It's really the empty state tree that would be the best indicator. + if block_height == 0 + && state_params.timestamp.0 == 0 + && state_params.network_version == NetworkVersion::V0 + { + return Ok(None); + } + + // Create MultiEngine (same as in App::new) + let multi_engine = Arc::new(MultiEngine::new(1)); + + // Create read-only execution state + let exec_state = FvmExecState::new( + ReadOnlyBlockstore::new(Arc::new(state_store.clone())), + multi_engine.as_ref(), + block_height as ChainEpoch, + state_params, + ) + .context("error creating execution state")?; + + Ok(Some(exec_state)) +} + +/// Query the F3 Light Client Actor state from a read-only execution state. +/// Returns the actor state if F3 is initialized, None otherwise. +pub fn query_f3_state( + exec_state: &mut FvmExecState>, +) -> Result> +where + BS: Blockstore + Clone + 'static + Send + Sync, +{ + let f3_caller = F3LightClientCaller::new(); + match f3_caller.get_state(exec_state) { + Ok(state) => Ok(Some(state)), + Err(e) => { + // F3 actor might not be deployed (non-Filecoin parent) + tracing::debug!("F3 Light Client Actor not found or not accessible: {}", e); + Ok(None) + } + } +} + impl App where KV: KVStore @@ -481,6 +552,12 @@ where .context("Validator cache is not available")? .get_validator(id) } + + /// Get access to the messages interpreter + /// Used to access the TopDownManager for updating the proof cache + pub fn interpreter(&self) -> &Arc { + &self.messages_interpreter + } } // NOTE: The `Application` interface doesn't allow failures at the moment. The protobuf diff --git a/fendermint/app/src/cmd/genesis.rs b/fendermint/app/src/cmd/genesis.rs index d0365eeb85..b4601dddb2 100644 --- a/fendermint/app/src/cmd/genesis.rs +++ b/fendermint/app/src/cmd/genesis.rs @@ -350,8 +350,9 @@ pub async fn seal_genesis(genesis_file: &PathBuf, args: &SealGenesisArgs) -> any builder.write_to(args.output_path.clone()).await } -/// Fetches F3 parameters for a specific instance ID from the parent Filecoin chain +/// Fetches F3 parameters from the parent Filecoin chain async fn fetch_f3_params_from_parent( + subnet_id: &SubnetID, parent_endpoint: &url::Url, parent_auth_token: Option<&String>, instance_id: u64, @@ -359,7 +360,7 @@ async fn fetch_f3_params_from_parent( tracing::info!( "Fetching F3 parameters for instance {} from parent chain at {}", instance_id, - parent_endpoint + parent_endpoint, ); let jsonrpc_client = JsonRpcClientImpl::new( @@ -367,9 +368,33 @@ async fn fetch_f3_params_from_parent( parent_auth_token.map(|s| s.as_str()), ); - // We use a dummy subnet ID here since F3 data is at the chain level, not subnet-specific + // We use a dummy subnet ID for the Lotus client since these RPC calls are chain-level, + // but the F3 network name derivation (for certificate fetch) uses the real subnet root. let lotus_client = LotusJsonRPCClient::new(jsonrpc_client, SubnetID::default()); + // Fetch the F3 certificate for the specific instance so we can deterministically + // derive the ECChain base epoch (the overlap point finalized by the previous certificate). + let cert = fendermint_vm_topdown_proof_service::fetch_certificate( + &parent_endpoint.to_string(), + subnet_id, + instance_id, + ) + .await + .context("failed to fetch F3 certificate for instance")?; + if cert.gpbft_instance != instance_id { + anyhow::bail!( + "F3 certificate instance mismatch: requested {}, got {}", + instance_id, + cert.gpbft_instance + ); + } + let base_epoch = cert.ec_chain.base().map(|b| b.epoch).ok_or_else(|| { + anyhow::anyhow!( + "F3 certificate instance {} has no ECChain base (cannot derive genesis base_epoch)", + instance_id + ) + })?; + // Get base power table for the specified instance let power_table_response = lotus_client.f3_get_power_table(instance_id).await?; @@ -383,6 +408,7 @@ async fn fetch_f3_params_from_parent( // Parse the power string to u64 let power = entry.power.parse::()?; Ok(types::PowerEntry { + id: entry.id, public_key: public_key_bytes, power, }) @@ -396,6 +422,7 @@ async fn fetch_f3_params_from_parent( ); Ok(Some(ipc::F3Params { instance_id, + base_epoch, power_table, })) } @@ -440,7 +467,9 @@ pub async fn new_genesis_from_parent( ) })?; + tracing::info!("Fetching F3 data from parent Filecoin chain"); fetch_f3_params_from_parent( + &args.subnet_id, parent_rpc, args.parent_filecoin_auth_token.as_ref(), f3_instance_id, diff --git a/fendermint/app/src/cmd/proof_cache.rs b/fendermint/app/src/cmd/proof_cache.rs index 38aa40e6a0..dcb4f861fe 100644 --- a/fendermint/app/src/cmd/proof_cache.rs +++ b/fendermint/app/src/cmd/proof_cache.rs @@ -5,6 +5,7 @@ use crate::cmd; use crate::options::proof_cache::{ProofCacheArgs, ProofCacheCommands}; use fendermint_vm_topdown_proof_service::persistence::ProofCachePersistence; use std::path::Path; +use std::path::PathBuf; cmd! { ProofCacheArgs(self) { @@ -24,7 +25,7 @@ fn handle_proof_cache_command(args: &ProofCacheArgs) -> anyhow::Result<()> { } } -fn inspect_cache(db_path: &Path) -> anyhow::Result<()> { +fn inspect_cache(db_path: &PathBuf) -> anyhow::Result<()> { println!("=== Proof Cache Inspection ==="); println!("Database: {}", db_path.display()); println!(); @@ -69,7 +70,7 @@ fn inspect_cache(db_path: &Path) -> anyhow::Result<()> { Ok(()) } -fn show_stats(db_path: &Path) -> anyhow::Result<()> { +fn show_stats(db_path: &PathBuf) -> anyhow::Result<()> { println!("=== Proof Cache Statistics ==="); println!("Database: {}", db_path.display()); println!(); @@ -125,7 +126,7 @@ fn show_stats(db_path: &Path) -> anyhow::Result<()> { Ok(()) } -fn get_proof(db_path: &Path, instance_id: u64) -> anyhow::Result<()> { +fn get_proof(db_path: &PathBuf, instance_id: u64) -> anyhow::Result<()> { println!("=== Get Proof for Instance {} ===", instance_id); println!("Database: {}", db_path.display()); println!(); diff --git a/fendermint/app/src/service/mod.rs b/fendermint/app/src/service/mod.rs index a0af14723f..9604dea451 100644 --- a/fendermint/app/src/service/mod.rs +++ b/fendermint/app/src/service/mod.rs @@ -3,3 +3,4 @@ pub mod eth_api; pub mod node; +mod topdown; diff --git a/fendermint/app/src/service/node.rs b/fendermint/app/src/service/node.rs index d2baffacd8..ffa4af6ffa 100644 --- a/fendermint/app/src/service/node.rs +++ b/fendermint/app/src/service/node.rs @@ -2,41 +2,26 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::{anyhow, bail, Context}; -use async_stm::atomically_or_err; use fendermint_abci::ApplicationService; -use fendermint_crypto::SecretKey; use fendermint_rocksdb::{blockstore::NamespaceBlockstore, namespaces, RocksDb, RocksDbConfig}; -use fendermint_vm_actor_interface::eam::EthAddress; use fendermint_vm_interpreter::fvm::interpreter::FvmMessagesInterpreter; use fendermint_vm_interpreter::fvm::observe::register_metrics as register_interpreter_metrics; -use fendermint_vm_interpreter::fvm::topdown::TopDownManager; use fendermint_vm_interpreter::fvm::upgrades::UpgradeScheduler; use fendermint_vm_snapshot::{SnapshotManager, SnapshotParams}; use fendermint_vm_topdown::observe::register_metrics as register_topdown_metrics; -use fendermint_vm_topdown::proxy::{IPCProviderProxy, IPCProviderProxyWithLatency}; -use fendermint_vm_topdown::sync::launch_polling_syncer; -use fendermint_vm_topdown::voting::{publish_vote_loop, Error as VoteError, VoteTally}; -use fendermint_vm_topdown::{CachedFinalityProvider, IPCParentFinality, Toggle}; use fvm_shared::address::{current_network, Address, Network}; -use ipc_ipld_resolver::{Event as ResolverEvent, VoteRecord}; use ipc_observability::observe::register_metrics as register_default_metrics; -use ipc_provider::config::subnet::{EVMSubnet, SubnetConfig}; -use ipc_provider::IpcProvider; -use libp2p::identity::secp256k1; -use libp2p::identity::Keypair; -use std::sync::Arc; use tokio::select; -use tokio::sync::broadcast::error::RecvError; use tokio_util::sync::CancellationToken; use tower::ServiceBuilder; use tracing::info; use crate::cmd::key::read_secret_key; -use crate::ipc::{AppParentFinalityQuery, AppVote}; use crate::observe::register_metrics as register_consensus_metrics; -use crate::{App, AppConfig, AppStore, BitswapBlockstore}; +use crate::{App, AppConfig, AppStore}; use fendermint_app_settings::{AccountKind, Settings}; +use super::topdown::start_topdown_if_enabled; use fendermint_vm_interpreter::fvm::end_block_hook::EndBlockManager; // Database collection names. @@ -121,102 +106,16 @@ pub async fn run( let state_store = NamespaceBlockstore::new(db.clone(), ns.state_store).context("error creating state DB")?; - let parent_finality_votes = VoteTally::empty(); - - let topdown_enabled = settings.topdown_enabled(); - - // If enabled, start a resolver that communicates with the application through the resolve pool. - if settings.resolver_enabled() { - let mut service = - make_resolver_service(&settings, db.clone(), state_store.clone(), ns.bit_store)?; - - // Register all metrics from the IPLD resolver stack - if let Some(ref registry) = metrics_registry { - service - .register_metrics(registry) - .context("failed to register IPLD resolver metrics")?; - } - - let client = service.client(); - - let own_subnet_id = settings.ipc.subnet_id.clone(); - - client - .add_provided_subnet(own_subnet_id.clone()) - .context("error adding own provided subnet.")?; - - if topdown_enabled { - if let Some(key) = validator_keypair { - let parent_finality_votes = parent_finality_votes.clone(); - - tracing::info!("starting the parent finality vote gossip loop..."); - tokio::spawn(async move { - publish_vote_loop( - parent_finality_votes, - settings.ipc.vote_interval, - settings.ipc.vote_timeout, - key, - own_subnet_id, - client, - |height, block_hash| { - AppVote::ParentFinality(IPCParentFinality { height, block_hash }) - }, - ) - .await - }); - } - } else { - tracing::info!("parent finality vote gossip disabled"); - } - - tracing::info!("subscribing to gossip..."); - let rx = service.subscribe(); - let parent_finality_votes = parent_finality_votes.clone(); - tokio::spawn(async move { - dispatch_resolver_events(rx, parent_finality_votes, topdown_enabled).await; - }); - - tracing::info!("starting the IPLD Resolver Service..."); - tokio::spawn(async move { - if let Err(e) = service.run().await { - tracing::error!("IPLD Resolver Service failed: {e:#}") - } - }); - } else { - tracing::info!("IPLD Resolver disabled.") - } - - let (parent_finality_provider, ipc_tuple) = if topdown_enabled { - info!("topdown finality enabled"); - let topdown_config = settings.ipc.topdown_config()?; - let mut config = fendermint_vm_topdown::Config::new( - topdown_config.chain_head_delay, - topdown_config.polling_interval, - topdown_config.exponential_back_off, - topdown_config.exponential_retry_limit, - ) - .with_proposal_delay(topdown_config.proposal_delay) - .with_max_proposal_range(topdown_config.max_proposal_range); - - if let Some(v) = topdown_config.max_cache_blocks { - info!(value = v, "setting max cache blocks"); - config = config.with_max_cache_blocks(v); - } - - let ipc_provider = { - let p = make_ipc_provider_proxy(&settings)?; - Arc::new(IPCProviderProxyWithLatency::new(p)) - }; - - let finality_provider = - CachedFinalityProvider::uninitialized(config.clone(), ipc_provider.clone()).await?; - - let p = Arc::new(Toggle::enabled(finality_provider)); - (p, Some((ipc_provider, config))) - } else { - info!("topdown finality disabled"); - (Arc::new(Toggle::disabled()), None) - }; + let topdown = start_topdown_if_enabled( + &settings, + &db, + &state_store, + ns.app.clone(), + ns.bit_store.clone(), + validator_keypair, + metrics_registry.as_ref(), + ) + .await?; // Start a snapshot manager in the background. let snapshots = if settings.snapshots.enabled { @@ -245,14 +144,9 @@ pub async fn run( }; let end_block_manager = EndBlockManager::new(); - let top_down_manager = TopDownManager::new( - parent_finality_provider.clone(), - parent_finality_votes.clone(), - ); - let interpreter = FvmMessagesInterpreter::new( end_block_manager, - top_down_manager, + topdown.manager(), UpgradeScheduler::new(), testing_settings.is_none_or(|t| t.push_chain_meta), settings.abci.block_max_msgs, @@ -274,24 +168,9 @@ pub async fn run( snapshots, )?; - if let Some((agent_proxy, config)) = ipc_tuple { - let app_parent_finality_query = AppParentFinalityQuery::new(app.clone()); - tokio::spawn(async move { - match launch_polling_syncer( - app_parent_finality_query, - config, - parent_finality_provider, - parent_finality_votes, - agent_proxy, - tendermint_client, - ) - .await - { - Ok(_) => {} - Err(e) => tracing::error!("cannot launch polling syncer: {e}"), - } - }); - } + topdown + .spawn_legacy_polling_syncer_if_needed(app.clone(), tendermint_client.clone()) + .await?; // Start the metrics on a background thread. if let Some(registry) = metrics_registry { @@ -370,173 +249,12 @@ fn open_db(settings: &Settings, ns: &Namespaces) -> anyhow::Result { Ok(db) } -fn make_resolver_service( - settings: &Settings, - db: RocksDb, - state_store: NamespaceBlockstore, - bit_store_ns: String, -) -> anyhow::Result> { - // Blockstore for Bitswap. - let bit_store = NamespaceBlockstore::new(db, bit_store_ns).context("error creating bit DB")?; - - // Blockstore for Bitswap with a fallback on the actor store for reads. - let bitswap_store = BitswapBlockstore::new(state_store, bit_store); - - let config = to_resolver_config(settings).context("error creating resolver config")?; - - let service = ipc_ipld_resolver::Service::new(config, bitswap_store) - .context("error creating IPLD Resolver Service")?; - - Ok(service) -} - -fn make_ipc_provider_proxy(settings: &Settings) -> anyhow::Result { - let topdown_config = settings.ipc.topdown_config()?; - let subnet = ipc_provider::config::Subnet { - id: settings - .ipc - .subnet_id - .parent() - .ok_or_else(|| anyhow!("subnet has no parent"))?, - config: SubnetConfig::Fevm(EVMSubnet { - provider_http: topdown_config - .parent_http_endpoint - .to_string() - .parse() - .unwrap(), - provider_timeout: topdown_config.parent_http_timeout, - auth_token: topdown_config.parent_http_auth_token.as_ref().cloned(), - registry_addr: topdown_config.parent_registry, - gateway_addr: topdown_config.parent_gateway, - }), - }; - info!("init ipc provider with subnet: {}", subnet.id); - - let ipc_provider = IpcProvider::new_with_subnet(None, subnet)?; - IPCProviderProxy::new(ipc_provider, settings.ipc.subnet_id.clone()) -} - -fn to_resolver_config(settings: &Settings) -> anyhow::Result { - use ipc_ipld_resolver::{ - Config, ConnectionConfig, ContentConfig, DiscoveryConfig, MembershipConfig, NetworkConfig, - }; - - let r = &settings.resolver; - - let local_key: Keypair = { - let path = r.network.local_key(settings.home_dir()); - let sk = read_secret_key(&path)?; - let sk = secp256k1::SecretKey::try_from_bytes(sk.serialize())?; - secp256k1::Keypair::from(sk).into() - }; - - let network_name = format!( - "ipld-resolver-{}-{}", - settings.ipc.subnet_id.root_id(), - r.network.network_name - ); - - let config = Config { - connection: ConnectionConfig { - listen_addr: r.connection.listen_addr.clone(), - external_addresses: r.connection.external_addresses.clone(), - expected_peer_count: r.connection.expected_peer_count, - max_incoming: r.connection.max_incoming, - max_peers_per_query: r.connection.max_peers_per_query, - event_buffer_capacity: r.connection.event_buffer_capacity, - }, - network: NetworkConfig { - local_key, - network_name, - }, - discovery: DiscoveryConfig { - static_addresses: r.discovery.static_addresses.clone(), - target_connections: r.discovery.target_connections, - enable_kademlia: r.discovery.enable_kademlia, - }, - membership: MembershipConfig { - static_subnets: r.membership.static_subnets.clone(), - max_subnets: r.membership.max_subnets, - publish_interval: r.membership.publish_interval, - min_time_between_publish: r.membership.min_time_between_publish, - max_provider_age: r.membership.max_provider_age, - }, - content: ContentConfig { - rate_limit_bytes: r.content.rate_limit_bytes, - rate_limit_period: r.content.rate_limit_period, - }, - }; - - Ok(config) -} - -fn to_address(sk: &SecretKey, kind: &AccountKind) -> anyhow::Result
{ +fn to_address(sk: &fendermint_crypto::SecretKey, kind: &AccountKind) -> anyhow::Result
{ let pk = sk.public_key().serialize(); match kind { AccountKind::Regular => Ok(Address::new_secp256k1(&pk)?), - AccountKind::Ethereum => Ok(Address::from(EthAddress::new_secp256k1(&pk)?)), - } -} - -async fn dispatch_resolver_events( - mut rx: tokio::sync::broadcast::Receiver>, - parent_finality_votes: VoteTally, - topdown_enabled: bool, -) { - loop { - match rx.recv().await { - Ok(event) => match event { - ResolverEvent::ReceivedPreemptive(_, _) => {} - ResolverEvent::ReceivedVote(vote) => { - dispatch_vote(*vote, &parent_finality_votes, topdown_enabled).await; - } - }, - Err(RecvError::Lagged(n)) => { - tracing::warn!("the resolver service skipped {n} gossip events") - } - Err(RecvError::Closed) => { - tracing::error!("the resolver service stopped receiving gossip"); - return; - } - } - } -} - -async fn dispatch_vote( - vote: VoteRecord, - parent_finality_votes: &VoteTally, - topdown_enabled: bool, -) { - match vote.content { - AppVote::ParentFinality(f) => { - if !topdown_enabled { - tracing::debug!("ignoring vote; topdown disabled"); - return; - } - let res = atomically_or_err(|| { - parent_finality_votes.add_vote( - vote.public_key.clone(), - f.height, - f.block_hash.clone(), - ) - }) - .await; - - match res { - Err(e @ VoteError::Equivocation(_, _, _, _)) => { - tracing::warn!(error = e.to_string(), "failed to handle vote"); - } - Err(e @ ( - VoteError::Uninitialized // early vote, we're not ready yet - | VoteError::UnpoweredValidator(_) // maybe arrived too early or too late, or spam - | VoteError::UnexpectedBlock(_, _) // won't happen here - )) => { - tracing::debug!(error = e.to_string(), "failed to handle vote"); - } - _ => { - tracing::debug!("vote handled"); - } - }; - } + AccountKind::Ethereum => Ok(Address::from( + fendermint_vm_actor_interface::eam::EthAddress::new_secp256k1(&pk)?, + )), } } diff --git a/fendermint/app/src/service/topdown.rs b/fendermint/app/src/service/topdown.rs new file mode 100644 index 0000000000..5ca0527b40 --- /dev/null +++ b/fendermint/app/src/service/topdown.rs @@ -0,0 +1,478 @@ +// Copyright 2022-2026 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use std::sync::Arc; + +use anyhow::{anyhow, bail, Context}; +use async_stm::atomically_or_err; +use fendermint_rocksdb::blockstore::NamespaceBlockstore; +use fendermint_rocksdb::RocksDb; +use fendermint_vm_interpreter::fvm::interpreter::FvmMessagesInterpreter; +use fendermint_vm_interpreter::fvm::topdown::TopDownManager; +use fendermint_vm_interpreter::fvm::LegacyTopDownHandler; +use fendermint_vm_topdown::proxy::{IPCProviderProxy, IPCProviderProxyWithLatency}; +use fendermint_vm_topdown::sync::launch_polling_syncer; +use fendermint_vm_topdown::voting::{publish_vote_loop, Error as VoteError, VoteTally}; +use fendermint_vm_topdown::{CachedFinalityProvider, IPCParentFinality, Toggle}; +use ipc_api::subnet_id::SubnetID; +use ipc_ipld_resolver::{Event as ResolverEvent, VoteRecord}; +use ipc_provider::config::subnet::{EVMSubnet, SubnetConfig}; +use ipc_provider::IpcProvider; +use libp2p::identity::secp256k1; +use libp2p::identity::Keypair; +use tokio::sync::broadcast::error::RecvError; + +use crate::cmd::key::read_secret_key; +use crate::ipc::AppParentFinalityQuery; +use crate::ipc::AppVote; +use crate::{App, AppStore, BitswapBlockstore}; +use fendermint_app_settings::{Settings, TopDownSettings}; +use fendermint_storage::KVStore; + +type TopDownFinalityProvider = Arc>>; + +/// Legacy topdown background tasks which require a live `App` instance. +struct LegacyPostInit { + agent_proxy: Arc, + config: fendermint_vm_topdown::Config, + parent_finality_provider: TopDownFinalityProvider, + parent_finality_votes: VoteTally, +} + +/// Result of topdown initialization performed before `App::new()`. +pub(super) struct TopDownInit { + manager: TopDownManager, + legacy_post_init: Option, +} + +impl TopDownInit { + pub(super) fn manager(&self) -> TopDownManager { + self.manager.clone() + } + + pub(super) async fn spawn_legacy_polling_syncer_if_needed( + self, + app: App< + RocksDb, + NamespaceBlockstore, + AppStore, + FvmMessagesInterpreter, + >, + tendermint_client: tendermint_rpc::HttpClient, + ) -> anyhow::Result<()> { + if let Some(p) = self.legacy_post_init { + let app_parent_finality_query = AppParentFinalityQuery::new(app); + tokio::spawn(async move { + match launch_polling_syncer( + app_parent_finality_query, + p.config, + p.parent_finality_provider, + p.parent_finality_votes, + p.agent_proxy, + tendermint_client, + ) + .await + { + Ok(_) => {} + Err(e) => tracing::error!("cannot launch polling syncer: {e}"), + } + }); + } + Ok(()) + } +} + +/// Initialize topdown (legacy or F3) before creating the `App`. +/// +/// Returns the `TopDownManager` to be put into the interpreter, and a `TopDownInit` handle +/// with any required post-`App::new()` work. +pub(super) async fn start_topdown_if_enabled( + settings: &Settings, + db: &RocksDb, + state_store: &NamespaceBlockstore, + app_namespace: ::Namespace, + bit_store_namespace: String, + validator_keypair: Option, + metrics_registry: Option<&prometheus::Registry>, +) -> anyhow::Result { + // If topdown is disabled, return a disabled topdown manager and no post-init tasks. + if !settings.topdown_enabled() { + return Ok(TopDownInit { + manager: TopDownManager::disabled(), + legacy_post_init: None, + }); + } + + let topdown_config = settings + .ipc + .topdown_config() + .context("topdown is enabled but topdown config is missing")?; + + let f3_enabled_in_config = topdown_config.f3.is_some(); + let f3_state_in_genesis = query_f3_state_in_genesis(db, state_store, app_namespace.clone())?; + + // Fail-fast consistency between config and committed/genesis state. + // + // - If genesis has F3 state, config must enable F3. + // - If config enables F3, genesis must have initial F3 state. + if f3_state_in_genesis.is_some() && !f3_enabled_in_config { + bail!("F3 is enabled in genesis but not in config"); + } + if f3_enabled_in_config && f3_state_in_genesis.is_none() { + bail!("F3 is enabled in config but initial F3 state is missing in genesis"); + } + + if f3_enabled_in_config { + return start_f3_topdown(settings, topdown_config, f3_state_in_genesis).await; + } + + start_legacy_topdown( + settings, + topdown_config, + validator_keypair, + db.clone(), + state_store.clone(), + bit_store_namespace, + metrics_registry, + ) + .await +} + +fn query_f3_state_in_genesis( + db: &RocksDb, + state_store: &NamespaceBlockstore, + app_namespace: ::Namespace, +) -> anyhow::Result> { + // Query F3 state from committed/genesis state once (used for fail-fast + F3 cache init). + let exec_state = + crate::app::create_read_only_exec_state::<_, _, AppStore>(db, state_store, app_namespace) + .context("failed to create read-only exec state")?; + + let f3_state_in_genesis = match exec_state { + Some(mut state) => crate::app::query_f3_state(&mut state) + .context("failed to query F3 state from genesis")?, + None => None, + }; + + Ok(f3_state_in_genesis) +} + +fn make_resolver_service( + settings: &Settings, + db: RocksDb, + state_store: NamespaceBlockstore, + bit_store_ns: String, +) -> anyhow::Result> { + let bit_store = NamespaceBlockstore::new(db, bit_store_ns).context("error creating bit DB")?; + let bitswap_store = BitswapBlockstore::new(state_store, bit_store); + let config = to_resolver_config(settings).context("error creating resolver config")?; + let service = ipc_ipld_resolver::Service::new(config, bitswap_store) + .context("error creating IPLD Resolver Service")?; + Ok(service) +} + +fn make_ipc_provider_proxy(settings: &Settings) -> anyhow::Result { + let topdown_config = settings.ipc.topdown_config()?; + let subnet = ipc_provider::config::Subnet { + id: settings + .ipc + .subnet_id + .parent() + .ok_or_else(|| anyhow!("subnet has no parent"))?, + config: SubnetConfig::Fevm(EVMSubnet { + provider_http: topdown_config + .parent_http_endpoint + .to_string() + .parse() + .unwrap(), + provider_timeout: topdown_config.parent_http_timeout, + auth_token: topdown_config.parent_http_auth_token.as_ref().cloned(), + registry_addr: topdown_config.parent_registry, + gateway_addr: topdown_config.parent_gateway, + }), + }; + + tracing::info!("init ipc provider with subnet: {}", subnet.id); + let ipc_provider = IpcProvider::new_with_subnet(None, subnet)?; + IPCProviderProxy::new(ipc_provider, settings.ipc.subnet_id.clone()) +} + +async fn start_legacy_topdown( + settings: &Settings, + topdown_config: &TopDownSettings, + validator_keypair: Option, + db: RocksDb, + state_store: NamespaceBlockstore, + bit_store_ns: String, + metrics_registry: Option<&prometheus::Registry>, +) -> anyhow::Result { + let parent_finality_votes = VoteTally::empty(); + // Resolver is required for legacy mode (vote gossip + quorum collection). + if !settings.resolver_enabled() { + bail!("IPLD Resolver is disabled but legacy topdown is enabled"); + } + + let mut service = make_resolver_service(settings, db, state_store.clone(), bit_store_ns)?; + + if let Some(registry) = metrics_registry { + service + .register_metrics(registry) + .context("failed to register IPLD resolver metrics")?; + } + + let client = service.client(); + let own_subnet_id = settings.ipc.subnet_id.clone(); + + client + .add_provided_subnet(own_subnet_id.clone()) + .context("error adding own provided subnet.")?; + + let key = validator_keypair.context("validator key missing but legacy topdown is enabled")?; + let parent_finality_votes_for_votes_loop = parent_finality_votes.clone(); + let vote_interval = settings.ipc.vote_interval; + let vote_timeout = settings.ipc.vote_timeout; + tracing::info!("starting the parent finality vote gossip loop..."); + tokio::spawn(async move { + publish_vote_loop( + parent_finality_votes_for_votes_loop, + vote_interval, + vote_timeout, + key, + own_subnet_id, + client, + |height, block_hash| AppVote::ParentFinality(IPCParentFinality { height, block_hash }), + ) + .await + }); + + tracing::info!("subscribing to gossip..."); + let rx = service.subscribe(); + let parent_finality_votes_for_resolver = parent_finality_votes.clone(); + tokio::spawn(async move { + dispatch_resolver_events(rx, parent_finality_votes_for_resolver).await; + }); + + tracing::info!("starting the IPLD Resolver Service..."); + tokio::spawn(async move { + if let Err(e) = service.run().await { + tracing::error!("IPLD Resolver Service failed: {e:#}") + } + }); + + tracing::info!("legacy topdown finality enabled"); + + let mut config = fendermint_vm_topdown::Config::new( + topdown_config.chain_head_delay, + topdown_config.polling_interval, + topdown_config.exponential_back_off, + topdown_config.exponential_retry_limit, + ) + .with_proposal_delay(topdown_config.proposal_delay) + .with_max_proposal_range(topdown_config.max_proposal_range); + + if let Some(v) = topdown_config.max_cache_blocks { + tracing::info!(value = v, "setting max cache blocks"); + config = config.with_max_cache_blocks(v); + } + + let ipc_provider = { + let p = make_ipc_provider_proxy(settings)?; + Arc::new(IPCProviderProxyWithLatency::new(p)) + }; + + let finality_provider = + CachedFinalityProvider::uninitialized(config.clone(), ipc_provider.clone()).await?; + + let parent_finality_provider: TopDownFinalityProvider = + Arc::new(Toggle::enabled(finality_provider)); + + let manager = TopDownManager::legacy(LegacyTopDownHandler::new( + parent_finality_provider.clone(), + parent_finality_votes.clone(), + )); + + Ok(TopDownInit { + manager, + legacy_post_init: Some(LegacyPostInit { + agent_proxy: ipc_provider, + config, + parent_finality_provider, + parent_finality_votes: parent_finality_votes.clone(), + }), + }) +} + +async fn start_f3_topdown( + settings: &Settings, + topdown_config: &TopDownSettings, + f3_state_in_genesis: Option, +) -> anyhow::Result { + let f3_config = topdown_config + .f3 + .as_ref() + .context("F3 is enabled in config but missing F3 config section")?; + + let f3_state = f3_state_in_genesis + .context("F3 is enabled in config but initial F3 state is missing in genesis")?; + let initial_instance = f3_state.latest_instance_id; + let initial_epoch = f3_state.latest_finalized_height.context( + "F3LightClientActor has no latest_finalized_height; genesis must set base epoch", + )?; + + let db_path = Some(settings.data_dir().join("proof-cache")); + let cache = Arc::new( + fendermint_vm_topdown_proof_service::ProofCache::new_with_persistence( + initial_epoch, + initial_instance, + f3_config.proof_service.cache_config.clone(), + db_path.as_ref().expect("db_path always set here"), + )?, + ); + + let handler = fendermint_vm_interpreter::fvm::F3TopDownHandler::new(cache); + let proof_cache = handler.proof_cache().clone(); + + let mut proof_config = f3_config.proof_service.clone(); + proof_config.parent_rpc_url = topdown_config.parent_http_endpoint.to_string(); + + if !proof_config.enabled { + tracing::info!("F3 proof service disabled in configuration"); + } else { + tracing::info!("F3 proof service enabled"); + + use fendermint_vm_topdown_proof_service::ProofGeneratorService; + let subnet_id: SubnetID = settings.ipc.subnet_id.clone(); + let service = ProofGeneratorService::new( + proof_config.clone(), + proof_cache.clone(), + &subnet_id, + initial_instance, + fendermint_vm_topdown_proof_service::power_entries_from_actor(&f3_state.power_table), + ) + .await + .context("Failed to create F3 proof service")?; + + tracing::info!( + f3_network = proof_config.f3_network_name(&subnet_id), + lookahead = proof_config.cache_config.lookahead_instances, + "F3 proof service initialized successfully" + ); + + tokio::spawn(async move { + service.run().await; + }); + } + + Ok(TopDownInit { + manager: TopDownManager::f3(handler), + legacy_post_init: None, + }) +} + +fn to_resolver_config(settings: &Settings) -> anyhow::Result { + use ipc_ipld_resolver::{ + Config, ConnectionConfig, ContentConfig, DiscoveryConfig, MembershipConfig, NetworkConfig, + }; + + let r = &settings.resolver; + + let local_key: Keypair = { + let path = r.network.local_key(settings.home_dir()); + let sk = read_secret_key(&path)?; + let sk = secp256k1::SecretKey::try_from_bytes(sk.serialize())?; + secp256k1::Keypair::from(sk).into() + }; + + let network_name = format!( + "ipld-resolver-{}-{}", + settings.ipc.subnet_id.root_id(), + r.network.network_name + ); + + Ok(Config { + connection: ConnectionConfig { + listen_addr: r.connection.listen_addr.clone(), + external_addresses: r.connection.external_addresses.clone(), + expected_peer_count: r.connection.expected_peer_count, + max_incoming: r.connection.max_incoming, + max_peers_per_query: r.connection.max_peers_per_query, + event_buffer_capacity: r.connection.event_buffer_capacity, + }, + network: NetworkConfig { + local_key, + network_name, + }, + discovery: DiscoveryConfig { + static_addresses: r.discovery.static_addresses.clone(), + target_connections: r.discovery.target_connections, + enable_kademlia: r.discovery.enable_kademlia, + }, + membership: MembershipConfig { + static_subnets: r.membership.static_subnets.clone(), + max_subnets: r.membership.max_subnets, + publish_interval: r.membership.publish_interval, + min_time_between_publish: r.membership.min_time_between_publish, + max_provider_age: r.membership.max_provider_age, + }, + content: ContentConfig { + rate_limit_bytes: r.content.rate_limit_bytes, + rate_limit_period: r.content.rate_limit_period, + }, + }) +} + +async fn dispatch_resolver_events( + mut rx: tokio::sync::broadcast::Receiver>, + parent_finality_votes: VoteTally, +) { + loop { + match rx.recv().await { + Ok(event) => match event { + ResolverEvent::ReceivedPreemptive(_, _) => {} + ResolverEvent::ReceivedVote(vote) => { + dispatch_vote(*vote, &parent_finality_votes).await; + } + }, + Err(RecvError::Lagged(n)) => { + tracing::warn!("the resolver service skipped {n} gossip events") + } + Err(RecvError::Closed) => { + tracing::error!("the resolver service stopped receiving gossip"); + return; + } + } + } +} + +async fn dispatch_vote(vote: VoteRecord, parent_finality_votes: &VoteTally) { + match vote.content { + AppVote::ParentFinality(f) => { + let res = atomically_or_err(|| { + parent_finality_votes.add_vote( + vote.public_key.clone(), + f.height, + f.block_hash.clone(), + ) + }) + .await; + + match res { + Err(e @ VoteError::Equivocation(_, _, _, _)) => { + tracing::warn!(error = e.to_string(), "failed to handle vote"); + } + Err( + e @ ( + VoteError::Uninitialized // early vote, we're not ready yet + | VoteError::UnpoweredValidator(_) // maybe arrived too early or too late, or spam + | VoteError::UnexpectedBlock(_, _) // won't happen here + ), + ) => { + tracing::debug!(error = e.to_string(), "failed to handle vote"); + } + _ => { + tracing::debug!("vote handled"); + } + }; + } + } +} diff --git a/fendermint/testing/contract-test/tests/gas_market.rs b/fendermint/testing/contract-test/tests/gas_market.rs index 8b57b8a16d..0341892b10 100644 --- a/fendermint/testing/contract-test/tests/gas_market.rs +++ b/fendermint/testing/contract-test/tests/gas_market.rs @@ -3,8 +3,6 @@ mod staking; -use std::sync::Arc; - use fendermint_actor_gas_market_eip1559::Constants; use fendermint_contract_test::Tester; use fendermint_crypto::{PublicKey, SecretKey}; @@ -14,14 +12,14 @@ use fendermint_vm_actor_interface::system::SYSTEM_ACTOR_ADDR; use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; +use fendermint_vm_interpreter::fvm::topdown::TopDownFinalityHandler; use fendermint_vm_interpreter::fvm::topdown::TopDownManager; use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; use fendermint_vm_interpreter::fvm::FvmMessagesInterpreter; use fendermint_vm_message::chain::ChainMessage; use fendermint_vm_message::conv::from_fvm; use fendermint_vm_message::signed::SignedMessage; -use fendermint_vm_topdown::voting::VoteTally; -use fendermint_vm_topdown::Toggle; +// Topdown is disabled for these tests. use fvm_shared::chainid::ChainID; use crate::staking::DEFAULT_CHAIN_ID; @@ -64,9 +62,7 @@ async fn tester_with_upgrader( let validator = rand_secret_key().public_key(); let end_block_manager = EndBlockManager::default(); - let finality_provider = Arc::new(Toggle::disabled()); - let vote_tally = VoteTally::empty(); - let top_down_manager = TopDownManager::new(finality_provider, vote_tally); + let top_down_manager = TopDownManager::new(TopDownFinalityHandler::Disabled); let interpreter: FvmMessagesInterpreter = FvmMessagesInterpreter::new( end_block_manager, diff --git a/fendermint/testing/contract-test/tests/run_upgrades.rs b/fendermint/testing/contract-test/tests/run_upgrades.rs index 0a734b426d..892aaefbe6 100644 --- a/fendermint/testing/contract-test/tests/run_upgrades.rs +++ b/fendermint/testing/contract-test/tests/run_upgrades.rs @@ -10,8 +10,6 @@ use fendermint_rpc::response::decode_fevm_return_data; use rand::rngs::StdRng; use rand::SeedableRng; use std::str::FromStr; -use std::sync::Arc; - use ethers::contract::abigen; use fvm_shared::address::Address; use fvm_shared::bigint::Zero; @@ -26,12 +24,12 @@ use fendermint_vm_core::Timestamp; use fendermint_vm_genesis::{Account, Actor, ActorMeta, Genesis, PermissionMode, SignerAddr}; use fendermint_vm_interpreter::fvm::end_block_hook::EndBlockManager; use fendermint_vm_interpreter::fvm::store::memory::MemoryBlockstore; +use fendermint_vm_interpreter::fvm::topdown::TopDownFinalityHandler; use fendermint_vm_interpreter::fvm::topdown::TopDownManager; use fendermint_vm_interpreter::fvm::upgrades::{Upgrade, UpgradeScheduler}; use fendermint_vm_interpreter::fvm::FvmMessagesInterpreter; use fendermint_vm_message::conv::from_fvm; -use fendermint_vm_topdown::voting::VoteTally; -use fendermint_vm_topdown::Toggle; +// Topdown is disabled for these tests. // returns a seeded secret key which is guaranteed to be the same every time fn my_secret_key() -> SecretKey { @@ -206,9 +204,7 @@ async fn test_applying_upgrades() { .unwrap(); let end_block_manager = EndBlockManager::default(); - let finality_provider = Arc::new(Toggle::disabled()); - let vote_tally = VoteTally::empty(); - let top_down_manager = TopDownManager::new(finality_provider, vote_tally); + let top_down_manager = TopDownManager::new(TopDownFinalityHandler::Disabled); let interpreter: FvmMessagesInterpreter = FvmMessagesInterpreter::new( end_block_manager, diff --git a/fendermint/vm/genesis/golden/genesis/cbor/genesis.cbor b/fendermint/vm/genesis/golden/genesis/cbor/genesis.cbor index 3a434547aa..9d73ac11e0 100644 --- a/fendermint/vm/genesis/golden/genesis/cbor/genesis.cbor +++ b/fendermint/vm/genesis/golden/genesis/cbor/genesis.cbor @@ -1 +1 @@ -ab63697063a16767617465776179a4697375626e65745f6964821b7eca9e45193a5edf834b008ff4db9f8db9fc98d0014b008c87f3a18bc3dad6a10156040a14b3436b1ce515269d0e01716511eb1e14a8f5b2736d616a6f726974795f70657263656e74616765184076626f74746f6d5f75705f636865636b5f706572696f641bedb1ca6774bbca0b776163746976655f76616c696461746f72735f6c696d69740f686163636f756e747382a2646d657461a1684d756c7469736967a4677369676e657273855501300fa088e63bc7284c7886986f0c0b32a4b8a43155017129d666aefc41888fa7848bc44d87e28e2627e45501394ae215967cb88cdff3028d154bb52de3dec828550109d2f2a1068fec62e2e1c03901b9d09e0fb6a1fb55011ffff67c4ddbe407cade15c5455c016c169a806d697468726573686f6c64026d76657374696e675f73746172741b7e691439f5a3af537076657374696e675f6475726174696f6e1b24ca3cfd0d53d7566762616c616e636540a2646d657461a1684d756c7469736967a4677369676e6572738555016591322f2dd8242769cbb1f25c7ea721a9a7af1655013434f6a3adaa76a3cc8eeb51e6ecf817422549d955017ccedef0ff205eb9de81a5195c611224864fe2af5501738bfd8efcf20518c5d1896a2c4dbf5a4532f23355018edcf2b088860ace2d2451fcda9a8cccf5155e6f697468726573686f6c64036d76657374696e675f73746172741b77bbd6777d7f09ff7076657374696e675f6475726174696f6e1b3cd419a08d6f82af6762616c616e636551009aceabcd73ffacc17e7a2589b0b410cb68626173655f6665654900664a46027308d50568636861696e5f696418656974696d657374616d701bb7e5d1db6ca339f66a636861696e5f6e616d65606a76616c696461746f727389a265706f7765725100f2e3e900debb4b13c01ef1ffbe73af006a7075626c69635f6b657958410430949f5cd9edf3887a6d162477f4adb9162c0aa02126099e4af4a6f18621449bbe65e7ea6383eb76f570ff3c9c7a4a4d939dc140efd778e2d5029c4a52f70ef7a265706f776572406a7075626c69635f6b65795841046f9029b17c045cd92f136b781ec69080903bf1963cc8dd61b464d0b523dce15d453bf839c66ccc7778a6a69e7dfab0d66ec05f28ae4a0f80d6abf9dae473ac0aa265706f7765725100571c2a44f074149d342219ca1c8b31566a7075626c69635f6b6579584104e2b47a1897aed91d422c992607879595875e6134dacab1a34c8037553f9c92e24eb009cfd2fb9f17fca7bf94e83df742c21ac47315d30a15e9c8a79eb2d39738a265706f7765725100ffffffffffffffff95e98069516d1c4d6a7075626c69635f6b65795841041baac9a539f651ce7653b132b3ce6f6d37252a271e67273c6c9622589f024506875c9b9c8ad30d47e87361a6564a9d24dffa4973c85b15299b9e91633c00c926a265706f7765725100e2ec5feec408b63f8f765b56f36f2a986a7075626c69635f6b65795841041691865a9fc99f8fd3bbc782337ffbddfd698fdf90347e8b7e17369baac381a0bf79510bea98c313af91778477d3c26d1fb21a9388d46f9c422b013b5a145a02a265706f7765725100ad363ad866db295a0e261113392461176a7075626c69635f6b657958410443ea92679151790e0864ec6349ad5d02e1b0540c04807de8c987da4d678089a456882ddfc884122288413a519b991954700d78432f8e830ea85747d6b313b785a265706f7765725100bf5c2b3032d672331f48c06fa88ea52d6a7075626c69635f6b6579584104837bc4a25a552f28beb82287674433a33354bb6c75f1b39ddbd3b553ce6fdcf4e270f933dc532d6b2c0ff34797cf9bb0d1f0a75b44017521132caf3bde7f2ed1a265706f776572510090288e0b0b66da00393724c1552d9acf6a7075626c69635f6b65795841047c4ab7ccc6f0b545a9be47a001de9e4f6bc3ef106d52ad97b1e242805009d89299e0534cd38040f0b38650219012aa96e0ba6686deaa1df7a71b07a2b38f6879a265706f776572510074551b9664b0fc66f17b34f9b90c6e926a7075626c69635f6b6579584104f05352e3e6ac2ab0c1298706964b05e0f1f81c81ed23d0ac6da795c45d67e28f322bbc85ad3cb5bc283b6b05a86d8b0898764f2d81a1a88210f99692334004e96b706f7765725f7363616c65006f6e6574776f726b5f76657273696f6e157365616d5f7065726d697373696f6e5f6d6f6465a1646d6f64656c756e72657374726963746564736970635f636f6e7472616374735f6f776e6572782a307830303030303030303030303030303030303030303030303030303030303030303030303030303030 \ No newline at end of file +ab63697063a16767617465776179a4697375626e65745f6964821b816f47d1e720d46080736d616a6f726974795f70657263656e74616765184776626f74746f6d5f75705f636865636b5f706572696f641b373b241b60288031776163746976655f76616c696461746f72735f6c696d697402686163636f756e747383a2646d657461a1674163636f756e74a1656f776e65725501ab4b11a63da573a968cefdcf54f67f59656838836762616c616e636540a2646d657461a1674163636f756e74a1656f776e657256040a2ac9b75d23b88f87379b2c873096cb0a5a5916376762616c616e636551007730724fc9960791c3cc42f0622dddc4a2646d657461a1674163636f756e74a1656f776e65725501de795fb38244abf232203bf0d631faaf93d044d86762616c616e63655100be4bc447f38b3cf7bfbeb8318a60ef7868626173655f6665654900f01f6c5469219bc668636861696e5f6964f66974696d657374616d701bd5d4c237928cb4466a636861696e5f6e616d65606a76616c696461746f727386a265706f77657251005c0d76e3f92f61b5d7b41f7cadbbb3fe6a7075626c69635f6b65795841046657731cfa7598b71db73c30d421397348b11855ef609858fb3fb6015f6ea21c760d378820bfbd06d3e9d96ba7081cf96ec960d44d1d1566b5852f13d4d1fe80a265706f776572510046af9b969cb119ea349cfeb7bcc43d6b6a7075626c69635f6b657958410436da1337304f41ceaf09c3cf908b6b5be7c97f22cd190b6043f8b4d2261eb22b938bf958aa9c22fb9ff02e44d58171caf64a039855a363e78bed5283ec55ed8aa265706f7765725100f582ba6fcffcf75920a71eee4fbe167d6a7075626c69635f6b65795841047429875cee5d219f38eebae8d65d5097dc53eb44312923692900bff1d11bee9327164a72119f6d2983ee4e337a94b4e07464705f1142014f194f411194bb8e81a265706f7765724900a95fb01ae9fb2bf56a7075626c69635f6b6579584104fcf23c5e3ab5918de6f7d4cf67c5984fe66033561205bda6b9c989c237bdbe3deb0911b9e387bb117cd295dd2be5f1ae75ebe5d6cacbdf18450d3161a4b38680a265706f7765725100ffffffffffffffff669c0cfc416a900e6a7075626c69635f6b6579584104c81d106d2bafa195ac7361f51f08660b3f08f19da481d383ccef5e66a4198c722971d18d73ca2cb1dadfd418b9aafa7e318b8de8cb3981ebaeb64d6b58951d1fa265706f7765725100d0fa9f65b0c384b5a78da5f1b384d4d56a7075626c69635f6b6579584104f7919a7af919638900477da71da2223722157852a40023da1887b6458c21f2c7012493e2f1ec92b214eff21ac44c2713f41472d2192cccf3f8377d00a210bf536b706f7765725f7363616c65006f6e6574776f726b5f76657273696f6e157365616d5f7065726d697373696f6e5f6d6f6465a1646d6f64656c756e72657374726963746564736970635f636f6e7472616374735f6f776e6572782a307834333433343334333433343334333433343334333433343334333433343334333433343334333433 \ No newline at end of file diff --git a/fendermint/vm/genesis/golden/genesis/cbor/genesis.txt b/fendermint/vm/genesis/golden/genesis/cbor/genesis.txt index 61f2dbfb46..2969c8b3f2 100644 --- a/fendermint/vm/genesis/golden/genesis/cbor/genesis.txt +++ b/fendermint/vm/genesis/golden/genesis/cbor/genesis.txt @@ -1 +1 @@ -Genesis { chain_name: "", chain_id: 101, timestamp: Timestamp(13251228218958232054), network_version: NetworkVersion(21), base_fee: TokenAmount(7.370780716479075589), power_scale: 0, validators: [Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [35734683, 19512417, 10085551, 41976984, 18230282, 33368942, 47276615, 63840745, 56416749, 795943], magnitude: 1, normalized: true }, y: Field { n: [49745655, 10949268, 59649360, 17022813, 26451393, 52335251, 51377097, 61725653, 65692547, 3119481], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(322856552237206793804.031172885867638528)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [64807261, 20196680, 30808902, 39383843, 9452529, 61973536, 20363137, 24339644, 28408836, 1827850], magnitude: 1, normalized: true }, y: Field { n: [7580682, 50230969, 16256362, 10664232, 40812639, 58633269, 40528359, 53599714, 3786348, 1134334], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(0.0)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [60592866, 906575, 52049096, 13855530, 25648737, 31581541, 46764640, 56915208, 35166126, 3714334], magnitude: 1, normalized: true }, y: Field { n: [47421240, 36300716, 10575516, 30168908, 46275268, 34569680, 41679182, 41705458, 30397179, 1289218], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(115789077268461795555.786014125701411158)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [50480390, 25728551, 41141961, 10254748, 20391210, 15965147, 20648747, 21445081, 27605494, 453298], magnitude: 1, normalized: true }, y: Field { n: [51494, 61102287, 22190521, 30351724, 14678601, 26388297, 53877349, 3481505, 60590803, 2217766], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(340282366920938463455.730169729109531725)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [46367136, 30254826, 65583073, 58605777, 33384847, 14679799, 62683171, 41828174, 39493577, 369761], magnitude: 1, normalized: true }, y: Field { n: [34888194, 46157526, 49923106, 38675281, 18854426, 32829595, 18315335, 51138238, 17558168, 3137108], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(301632854851889825874.9190748556565654)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [58755492, 32936793, 64916632, 3150337, 48345172, 40589120, 38716980, 31733793, 40341841, 1112740], magnitude: 1, normalized: true }, y: Field { n: [51623813, 30537132, 3205765, 17612346, 7343480, 48645717, 1287449, 4753953, 31443076, 1417739], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(230238020826023862616.805863604753817879)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [40885492, 15553779, 54123965, 28432326, 53695675, 30477544, 58861686, 12362490, 10639957, 2154225], magnitude: 1, normalized: true }, y: Field { n: [41889489, 53202679, 55710002, 23924741, 13758631, 32761580, 16725113, 11906224, 20175955, 3710014], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(254361114468202393482.250430575417402669)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [645266, 9478164, 47807262, 4306250, 57394159, 7841683, 65305088, 47519398, 63751920, 2036397], magnitude: 1, normalized: true }, y: Field { n: [59730041, 46262444, 31423089, 35355304, 48282214, 305829, 6619673, 17023694, 55366528, 2521108], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(191619404244571460019.048908483311016655)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [23585423, 31813911, 17483482, 34059407, 15857692, 26395000, 43544681, 11191044, 48490156, 3937492], magnitude: 1, normalized: true }, y: Field { n: [54527209, 40215692, 42475791, 11929222, 9991759, 35349186, 62304346, 47640736, 8760636, 821999], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(154632352284471841201.727616896507604626)) }], accounts: [Actor { meta: Multisig(Multisig { signers: [SignerAddr(Address("f1gah2bchghpdsqtdyq2mg6dalgkslrjbrtgaesia")), SignerAddr(Address("f1oeu5mzvo7rayrd5hqsf4itmh4khcmj7eji5xhya")), SignerAddr(Address("f1hffoefmwps4izx7takgrks5vfxr55sbif4ba23a")), SignerAddr(Address("f1bhjpfiigr7wgfyxbya4qdooqtyh3nip3elwgtmq")), SignerAddr(Address("f1d777m7cn3psapsw6cxcukxabnqljvadnlox6opq"))], threshold: 2, vesting_duration: 2650998388208949078, vesting_start: 9108833960500375379 }), balance: TokenAmount(0.0) }, Actor { meta: Multisig(Multisig { signers: [SignerAddr(Address("f1mwitelzn3asco2olwhzfy7vhegu2plyw5jmez7a")), SignerAddr(Address("f1gq2pni5nvj3khteo5ni6n3hyc5bcksoziofrtza")), SignerAddr(Address("f1pthn54h7ebpltxubuumvyyisesde7yvpvgg7qwi")), SignerAddr(Address("f1oof73dx46icrrrorrfvcytn7ljctf4rtv3lemxa")), SignerAddr(Address("f1r3opfmeiqyfm4ljekh6nvgumzt2rkxtpsugo5by"))], threshold: 3, vesting_duration: 4383156514696692399, vesting_start: 8627725319853246975 }), balance: TokenAmount(205774209073450626808.321982400535204043) }], eam_permission_mode: Unrestricted, ipc: Some(IpcParams { gateway: GatewayParams { subnet_id: SubnetID { root: 9136288813687660255, children: [Address("f015002037678599764495"), Address("f011650084465412981644"), Address("f410fcszug2y44uksnhioafywkepldykkr5nsgp42gai")] }, bottom_up_check_period: 17127693403555613195, majority_percentage: 64, active_validators_limit: 15 } }), ipc_contracts_owner: 0x0000000000000000000000000000000000000000, f3: None } \ No newline at end of file +Genesis { chain_name: "", chain_id: None, timestamp: Timestamp(15408153769078993990), network_version: NetworkVersion(21), base_fee: TokenAmount(17.302667403177597894), power_scale: 0, validators: [Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [57582108, 65896535, 25530291, 22527362, 55095576, 17321564, 57918221, 40033398, 52230773, 1676764], magnitude: 1, normalized: true }, y: Field { n: [13762176, 21742837, 22440792, 55653492, 24037728, 29493054, 43882170, 49552207, 59252927, 1934157], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(122358886857618383084.425213723209872382)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [35566123, 36517001, 11928639, 9122916, 65522047, 2284246, 10239225, 17251004, 53948495, 898692], magnitude: 1, normalized: true }, y: Field { n: [5631370, 55877883, 37648574, 39933581, 49695235, 23092338, 50521165, 9170559, 22588060, 2417406], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(93957767361394412868.518672058372341099)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [18607763, 3144820, 37130896, 17876132, 64771051, 26694693, 49000077, 8813795, 56421981, 1903201], magnitude: 1, normalized: true }, y: Field { n: [12291713, 63980645, 1372564, 24921352, 7627888, 44379448, 48554807, 28616207, 41030047, 640402], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(326339638945795449155.377711843387512445)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [62766653, 40005773, 64646044, 22562838, 65429555, 32597523, 58543350, 38156187, 6175413, 4144271], magnitude: 1, normalized: true }, y: Field { n: [11765376, 55335017, 32605264, 56306479, 41282533, 49904747, 19488210, 49038835, 28959623, 3850820], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(12.204667144838523893)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [1674354, 64461225, 20462798, 41325063, 54462705, 63052162, 53878609, 42358449, 7154607, 3278660], magnitude: 1, normalized: true }, y: Field { n: [9772319, 26434262, 2013931, 61025510, 36801421, 40550047, 33374603, 11716459, 26047434, 679028], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(340282366920938463452.321662323862966286)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [2224839, 32346467, 37593480, 21663744, 52565368, 57182349, 7854705, 26092545, 41613593, 4056166], magnitude: 1, normalized: true }, y: Field { n: [34651987, 31408168, 13582211, 55076019, 66327666, 18024900, 50274732, 38455379, 65204716, 18724], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(277780730297773331994.111486734304072917)) }], accounts: [Actor { meta: Account(Account { owner: SignerAddr(Address("f1vnfrdjr5uvz2s2go7xhvj5t7lfswqoedua5rbqa")) }), balance: TokenAmount(0.0) }, Actor { meta: Account(Account { owner: SignerAddr(Address("f410ffle3oxjdxchyon43fsdtbfwlbjnfsfrxblezpty")) }), balance: TokenAmount(158429680263722177111.518053171922329028) }, Actor { meta: Account(Account { owner: SignerAddr(Address("f13z4v7m4cisv7emrahpynmmp2v6j5argy2mzh7va")) }), balance: TokenAmount(252946722516379168151.698918250941312888) }], eam_permission_mode: Unrestricted, ipc: Some(IpcParams { gateway: GatewayParams { subnet_id: SubnetID { root: 9326752320159011936, children: [] }, bottom_up_check_period: 3979814395707949105, majority_percentage: 71, active_validators_limit: 2 } }), ipc_contracts_owner: 0x4343434343434343434343434343434343434343, f3: None } \ No newline at end of file diff --git a/fendermint/vm/genesis/golden/genesis/json/genesis.json b/fendermint/vm/genesis/golden/genesis/json/genesis.json index 4d642a5102..9ac34e8862 100644 --- a/fendermint/vm/genesis/golden/genesis/json/genesis.json +++ b/fendermint/vm/genesis/golden/genesis/json/genesis.json @@ -1,40 +1,68 @@ { - "chain_name": "U", - "chain_id": 101, - "timestamp": 12982167733895342124, + "chain_name": "â®§\t§LN", + "chain_id": null, + "timestamp": 9166950190161876321, "network_version": 21, - "base_fee": "20131727983098262839895554089832955311", - "power_scale": 3, + "base_fee": "92132748893613804737712518769207785210", + "power_scale": 0, "validators": [ { - "public_key": "BH4dgpBqtkh6sKhMXelu7LKYN2nb1UJSX6ZCYGPmtmsCM9y80iAUMj5wu4Yiu952Dv3Oq2Rrbt9h45EgRmHkFSc=", - "power": "40223257309125237738285695934834340379" + "public_key": "BJ2ICxPgqIpgPxTkWHeENqdDxgMukcg/4a1l8CH51rIabdszVkESNJQ61+NUNq2BqKn8jLA+pRRR+Lng3IODoq0=", + "power": "119680730293037942857664149531788261571" }, { - "public_key": "BBPbyO1PiFQ7AoLdOYNViHBJ+EF6FuhOYGAOTygqwuvdOt960J5TvCIjwX2UIm2vPm+9ILZarBWcTFXz8rQG9bQ=", - "power": "19706769749739782860581433033120308803" + "public_key": "BFChHN77vnsbUNg4/l06mVTfF6qvbV5kaqV2btTLpPUL9mBsgEKwGPlQJuWBAYBv6G9VPhwX7lrztjpayy7jhEk=", + "power": "35254282891067121059299756507316764434" + } + ], + "accounts": [ + { + "meta": { + "Account": { + "owner": "f410flavvsjwlqczzuapnpzwuo7ftq32nvr6kyrzvnna" + } + }, + "balance": "197011208717819580134214681012943466690" }, { - "public_key": "BGB9f7R3VPpLiWszpGgbG+BLbtllYDg1YGq0RMqmv9xhBz7RG6WVOSCmUem+TAVQkIX2tPT7ZcZF+FLkCoNHhOc=", - "power": "260894447470586868017041531430614024483" + "meta": { + "Account": { + "owner": "f410f4pd4avs4gwsdwh5cqgvwg7bhzk4gkyotlez27aa" + } + }, + "balance": "102137686532253315554542634423215460983" }, { - "public_key": "BA0y6qtkjqTG5IdQDRNT3yHZx4WI+6Ua+LFYuXdfczM/ocBhjNJQmQVpCjDvuwHoBPD0hzK4KxtdJe+lPa7a3hU=", - "power": "178349749897518691979494799247727256537" + "meta": { + "Account": { + "owner": "f410fk6imdbvngrp4vzmvb4g7k3vt4q3ivds72dm4yvq" + } + }, + "balance": "153374264530312302270000858553190959779" }, { - "public_key": "BPhrBPJqcNmmxgGlHirO5Tt03wYXcUKlHJVYuqdWzNew6RQby1tcXpqwyFxSazNt6cOQ0UNjXBa2RAnw9CWJieY=", - "power": "135063245844052113159013998821298664415" - } - ], - "accounts": [ + "meta": { + "Multisig": { + "signers": [ + "f1caopsapwuhdielvfbshr3tk3zhrtn7h4de6ubty", + "f1fi2ofihi7ynbrkshlnqnmg4fw5v7ftbjmq6xtsq", + "f1ozfuvcwlf2e3dznoeq23mag3xjdfmd3vw2sn2ri", + "f1u453i6v24omn6yffy75z4c6fs6p2fiwczt5rdui" + ], + "threshold": 2, + "vesting_duration": 3306299543311472807, + "vesting_start": 14249729871702503296 + } + }, + "balance": "309092999959441721106188818455929111123" + }, { "meta": { "Account": { - "owner": "f410fkutwqjirqgadmxm43p2myoi5m3gurnulc4uvrti" + "owner": "f1brtfivrytkaf6jnkj2bejpwdeausws6jnenpn6i" } }, - "balance": "68216112094381133411177903949584186607" + "balance": "268168639178429380690207903639563038123" } ], "eam_permission_mode": { @@ -42,11 +70,11 @@ }, "ipc": { "gateway": { - "subnet_id": "/r16315642738389104412", - "bottom_up_check_period": 11339269183869879227, - "majority_percentage": 73, - "active_validators_limit": 63 + "subnet_id": "/r7209731025411039793/f00", + "bottom_up_check_period": 1, + "majority_percentage": 59, + "active_validators_limit": 67 } }, - "ipc_contracts_owner": "0x8989898989898989898989898989898989898989" + "ipc_contracts_owner": "0x9191919191919191919191919191919191919191" } \ No newline at end of file diff --git a/fendermint/vm/genesis/golden/genesis/json/genesis.txt b/fendermint/vm/genesis/golden/genesis/json/genesis.txt index f37522ab4d..d43116ff43 100644 --- a/fendermint/vm/genesis/golden/genesis/json/genesis.txt +++ b/fendermint/vm/genesis/golden/genesis/json/genesis.txt @@ -1 +1 @@ -Genesis { chain_name: "U", chain_id: 101, timestamp: Timestamp(12982167733895342124), network_version: NetworkVersion(21), base_fee: TokenAmount(20131727983098262839.895554089832955311), power_scale: 3, validators: [Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [45509378, 9967865, 19266148, 57627913, 43530089, 39566124, 42255838, 19000002, 43018934, 2066272], magnitude: 1, normalized: true }, y: Field { n: [31724839, 4723096, 32906809, 26324411, 50187947, 49782147, 62415403, 13171138, 13770772, 849711], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(40223257309125237738.285695934834340379)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [46328797, 60017162, 15074816, 32005025, 4847681, 13984284, 3003288, 22080522, 15552392, 325362], magnitude: 1, normalized: true }, y: Field { n: [456116, 24968365, 22660293, 47803056, 40877344, 10185679, 1562946, 49318031, 47226451, 964574], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(19706769749739782860.581433033120308803)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [46128225, 17904297, 55969451, 26575072, 4943577, 33998584, 45300294, 65613349, 62158676, 1580895], magnitude: 1, normalized: true }, y: Field { n: [55018727, 12124832, 6578053, 64220567, 8779444, 50418724, 18783204, 14975641, 18589077, 118708], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(260894447470586868017.041531430614024483)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [57881407, 36593111, 28281621, 35909268, 31049605, 13957064, 7667921, 43195282, 44786830, 216250], magnitude: 1, normalized: true }, y: Field { n: [47898133, 65621867, 28693086, 13295788, 15791239, 46168577, 10686203, 40113572, 26006096, 2650136], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(178349749897518691979.494799247727256537)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [46978992, 36612565, 38914389, 6145290, 57990918, 45332814, 1724898, 57056024, 15886960, 4070081], magnitude: 1, normalized: true }, y: Field { n: [25790950, 41696521, 23815232, 17665392, 29593809, 46979962, 8766758, 24799939, 63658844, 3818758], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(135063245844052113159.013998821298664415)) }], accounts: [Actor { meta: Account(Account { owner: SignerAddr(Address("f410fkutwqjirqgadmxm43p2myoi5m3gurnulc4uvrti")) }), balance: TokenAmount(68216112094381133411.177903949584186607) }], eam_permission_mode: Unrestricted, ipc: Some(IpcParams { gateway: GatewayParams { subnet_id: SubnetID { root: 16315642738389104412, children: [] }, bottom_up_check_period: 11339269183869879227, majority_percentage: 73, active_validators_limit: 63 } }), ipc_contracts_owner: 0x8989898989898989898989898989898989898989, f3: None } \ No newline at end of file +Genesis { chain_name: "â®§\t§LN", chain_id: None, timestamp: Timestamp(9166950190161876321), network_version: NetworkVersion(21), base_fee: TokenAmount(92132748893613804737.71251876920778521), power_scale: 0, validators: [Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [30847514, 24905854, 66984662, 12207904, 54773251, 31526313, 21906823, 36274428, 51634344, 2580994], magnitude: 1, normalized: true }, y: Field { n: [58958509, 41432864, 21307275, 46201492, 11140236, 28008554, 25048387, 13783275, 55984402, 1799884], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(119680730293037942857.664149531788261571)) }, Validator { public_key: ValidatorKey(PublicKey(Affine { x: Field { n: [61142283, 26981682, 38185559, 45987193, 14620586, 55486037, 25399269, 32271683, 14613438, 1321031], magnitude: 1, normalized: true }, y: Field { n: [48464969, 43430603, 28261219, 7364537, 7296318, 6298618, 40785936, 6546752, 8405680, 4036635], magnitude: 1, normalized: true }, infinity: false })), power: Collateral(TokenAmount(35254282891067121059.299756507316764434)) }], accounts: [Actor { meta: Account(Account { owner: SignerAddr(Address("f410flavvsjwlqczzuapnpzwuo7ftq32nvr6kyrzvnna")) }), balance: TokenAmount(197011208717819580134.21468101294346669) }, Actor { meta: Account(Account { owner: SignerAddr(Address("f410f4pd4avs4gwsdwh5cqgvwg7bhzk4gkyotlez27aa")) }), balance: TokenAmount(102137686532253315554.542634423215460983) }, Actor { meta: Account(Account { owner: SignerAddr(Address("f410fk6imdbvngrp4vzmvb4g7k3vt4q3ivds72dm4yvq")) }), balance: TokenAmount(153374264530312302270.000858553190959779) }, Actor { meta: Multisig(Multisig { signers: [SignerAddr(Address("f1caopsapwuhdielvfbshr3tk3zhrtn7h4de6ubty")), SignerAddr(Address("f1fi2ofihi7ynbrkshlnqnmg4fw5v7ftbjmq6xtsq")), SignerAddr(Address("f1ozfuvcwlf2e3dznoeq23mag3xjdfmd3vw2sn2ri")), SignerAddr(Address("f1u453i6v24omn6yffy75z4c6fs6p2fiwczt5rdui"))], threshold: 2, vesting_duration: 3306299543311472807, vesting_start: 14249729871702503296 }), balance: TokenAmount(309092999959441721106.188818455929111123) }, Actor { meta: Account(Account { owner: SignerAddr(Address("f1brtfivrytkaf6jnkj2bejpwdeausws6jnenpn6i")) }), balance: TokenAmount(268168639178429380690.207903639563038123) }], eam_permission_mode: Unrestricted, ipc: Some(IpcParams { gateway: GatewayParams { subnet_id: SubnetID { root: 7209731025411039793, children: [Address("f00")] }, bottom_up_check_period: 1, majority_percentage: 59, active_validators_limit: 67 } }), ipc_contracts_owner: 0x9191919191919191919191919191919191919191, f3: None } \ No newline at end of file diff --git a/fendermint/vm/genesis/src/lib.rs b/fendermint/vm/genesis/src/lib.rs index 7de96303d7..a34eb1452a 100644 --- a/fendermint/vm/genesis/src/lib.rs +++ b/fendermint/vm/genesis/src/lib.rs @@ -285,6 +285,12 @@ pub mod ipc { pub struct F3Params { /// F3 instance ID from parent chain pub instance_id: u64, + /// Base epoch of the ECChain for `instance_id`. + /// + /// This is the overlap point: the last epoch finalized by the previous certificate. + /// We treat it as already finalized/committed at genesis, so the first epoch to + /// prove/execute is `base_epoch + 1`. + pub base_epoch: fvm_shared::clock::ChainEpoch, /// Power table for F3 consensus from parent chain pub power_table: Vec, } diff --git a/fendermint/vm/interpreter/Cargo.toml b/fendermint/vm/interpreter/Cargo.toml index b364e3c5f0..cab64a7f96 100644 --- a/fendermint/vm/interpreter/Cargo.toml +++ b/fendermint/vm/interpreter/Cargo.toml @@ -18,6 +18,7 @@ fendermint_vm_genesis = { path = "../genesis" } fendermint_vm_message = { path = "../message" } fendermint_vm_resolver = { path = "../resolver" } fendermint_vm_topdown = { path = "../topdown" } +fendermint_vm_topdown_proof_service = { path = "../topdown/proof-service" } fendermint_crypto = { path = "../../crypto" } fendermint_eth_hardhat = { path = "../../eth/hardhat" } fendermint_eth_deployer = { path = "../../eth/deployer" } @@ -39,6 +40,7 @@ ipc-observability = { path = "../../../ipc/observability" } async-trait = { workspace = true } async-stm = { workspace = true } anyhow = { workspace = true } +arc-swap = "1.6" base64 = { workspace = true } ethers = { workspace = true } hex = { workspace = true } @@ -75,11 +77,13 @@ quickcheck = { workspace = true, optional = true } rand = { workspace = true, optional = true } merkle-tree-rs = { path = "../../../ext/merkle-tree-rs" } +proofs = { git = "https://github.com/consensus-shipyard/ipc-filecoin-proofs", branch = "proofs" } [dev-dependencies] quickcheck = { workspace = true } quickcheck_macros = { workspace = true } tempfile = { workspace = true } +actors-builtin-car = { path = "../../actors-builtin-car" } fendermint_vm_interpreter = { path = ".", features = ["arb"] } fendermint_vm_message = { path = "../message", features = ["arb"] } diff --git a/fendermint/vm/interpreter/src/fvm/event_extraction.rs b/fendermint/vm/interpreter/src/fvm/event_extraction.rs new file mode 100644 index 0000000000..c95315879c --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/event_extraction.rs @@ -0,0 +1,316 @@ +// Copyright 2022-2026 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Event extraction from F3 proof bundles +//! +//! This module provides functionality to extract and decode events from proof bundles, +//! including topdown messages and validator change events. + +use anyhow::{anyhow, Context, Result}; +use ethers::abi::RawLog; +use ethers::contract::EthLogDecode; +use ethers::types as et; +use ipc_actors_abis::{lib_gateway, lib_power_change_log}; +use ipc_api::cross::IpcEnvelope; +use ipc_api::staking::PowerChangeRequest; +use proofs::proofs::common::bundle::UnifiedProofBundle; +use tracing::{debug, trace}; + +/// Extract topdown messages from a proof bundle +/// +/// This function iterates through event proofs in the bundle and extracts +/// NewTopDownMessage events by: +/// 1. Finding events matching the signature +/// 2. Decoding the IpcEnvelope from the event data using contract bindings +/// 3. Returning all extracted messages +pub fn extract_topdown_messages(proof_bundle: &UnifiedProofBundle) -> Result> { + let mut messages = Vec::new(); + + for event_proof in &proof_bundle.event_proofs { + let event_log = extract_event_from_proof(event_proof)?; + + // Try to decode as NewTopDownMessage event + if let Ok(event) = decode_topdown_message_event(&event_log) { + trace!( + emitter = event_log.emitter, + subnet = ?event.subnet, + "Found NewTopDownMessage event" + ); + + // Convert from contract binding type to IPC type + let envelope = IpcEnvelope::try_from(event.message) + .context("Failed to convert gateway IpcEnvelope to IPC IpcEnvelope")?; + messages.push(envelope); + } + } + + debug!( + message_count = messages.len(), + "Extracted topdown messages from proof bundle" + ); + + Ok(messages) +} + +/// Extract validator changes from a proof bundle +/// +/// This function iterates through event proofs and extracts +/// NewPowerChangeRequest events by: +/// 1. Finding events matching the signature +/// 2. Decoding the PowerChangeRequest from the event data using contract bindings +/// 3. Returning all extracted changes +pub fn extract_validator_changes( + proof_bundle: &UnifiedProofBundle, +) -> Result> { + let mut changes = Vec::new(); + + for event_proof in &proof_bundle.event_proofs { + let event_log = extract_event_from_proof(event_proof)?; + + // Try to decode as NewPowerChangeRequest event + if let Ok(event) = decode_power_change_event(&event_log) { + trace!( + emitter = event_log.emitter, + validator = ?event.validator, + op = event.op, + "Found NewPowerChangeRequest event" + ); + + // Convert to PowerChangeRequest + let change_request = PowerChangeRequest::try_from(event) + .context("Failed to convert power change event to PowerChangeRequest")?; + changes.push(change_request); + } + } + + debug!( + change_count = changes.len(), + "Extracted validator changes from proof bundle" + ); + + Ok(changes) +} + +/// Extract events from a single event proof +/// +/// The EventProof contains EventData which includes: +/// - emitter: actor ID that emitted the event +/// - topics: hex-encoded topics (event signature, indexed params) +/// - data: hex-encoded event data (often ABI encoded for cross-chain) +fn extract_event_from_proof( + event_proof: &proofs::proofs::events::bundle::EventProof, +) -> Result { + // Convert hex-encoded topics to H256 + let topics: Result> = event_proof + .event_data + .topics + .iter() + .map(|topic| { + // Remove 0x prefix if present and parse hex + let topic_str = topic.trim_start_matches("0x"); + let bytes = + hex::decode(topic_str).context(format!("Failed to decode topic hex: {}", topic))?; + + if bytes.len() != 32 { + return Err(anyhow!("Topic must be 32 bytes, got {} bytes", bytes.len())); + } + + Ok(et::H256::from_slice(&bytes)) + }) + .collect(); + + let topics = topics?; + + // Convert hex-encoded data + let data_str = event_proof.event_data.data.trim_start_matches("0x"); + let data = hex::decode(data_str).context(format!( + "Failed to decode event data hex: {}", + event_proof.event_data.data + ))?; + + Ok(EventLog { + emitter: event_proof.event_data.emitter, + topics, + data, + }) +} + +/// Helper struct to represent an event log +#[derive(Debug, Clone)] +struct EventLog { + emitter: u64, + topics: Vec, + data: Vec, +} + +/// Decode a NewTopDownMessage event using the contract bindings +fn decode_topdown_message_event( + event_log: &EventLog, +) -> Result { + // Create RawLog from our EventLog + let raw_log = RawLog { + topics: event_log.topics.clone(), + data: event_log.data.clone(), + }; + + // Use the contract binding's decoding + lib_gateway::NewTopDownMessageFilter::decode_log(&raw_log) + .map_err(|e| anyhow!("Failed to decode NewTopDownMessage event: {}", e)) +} + +/// Decode a NewPowerChangeRequest event using the contract bindings +fn decode_power_change_event( + event_log: &EventLog, +) -> Result { + // Create RawLog from our EventLog + let raw_log = RawLog { + topics: event_log.topics.clone(), + data: event_log.data.clone(), + }; + + // Use the contract binding's decoding + lib_power_change_log::NewPowerChangeRequestFilter::decode_log(&raw_log) + .map_err(|e| anyhow!("Failed to decode NewPowerChangeRequest event: {}", e)) +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers::abi::Tokenizable; + use ethers::abi::{encode, Token}; + use ethers::contract::EthEvent; + use ethers::types::{Address as EthAddress, H256, U256}; + use fvm_shared::address::Address as FilAddress; + use fvm_shared::econ::TokenAmount; + use fvm_shared::ActorID; + use ipc_actors_abis::lib_gateway; + use ipc_actors_abis::lib_power_change_log; + use ipc_api::address::IPCAddress; + use ipc_api::cross::{IpcEnvelope as ApiIpcEnvelope, IpcMsgKind}; + use ipc_api::ethers_address_to_fil_address; + use ipc_api::subnet_id::SubnetID; + use proofs::proofs::common::bundle::UnifiedProofBundle; + use proofs::proofs::events::bundle::{EventData, EventProof}; + + fn h256_to_0x_string(h: H256) -> String { + format!("0x{}", hex::encode(h.as_bytes())) + } + + fn bytes_to_0x_string(b: &[u8]) -> String { + format!("0x{}", hex::encode(b)) + } + + fn mk_event_proof(topics: Vec, data: String) -> EventProof { + EventProof { + parent_epoch: 100, + child_epoch: 101, + parent_tipset_cids: vec!["bafy...parent".to_string()], + child_block_cid: "bafy...child".to_string(), + message_cid: "bafy...msg".to_string(), + exec_index: 0, + event_index: 0, + event_data: EventData { + emitter: 1000, + topics, + data, + }, + } + } + + #[test] + fn extracts_and_decodes_new_topdown_message_event() -> Result<()> { + // Build a valid IPC envelope, then convert into the EVM binding struct. + // This avoids guessing the `FvmAddress` encoding. + let child_route = FilAddress::new_delegated(10 as ActorID, &[0x11; 20]) + .context("failed to create delegated route address")?; + let subnet_id = SubnetID::new(314159, vec![child_route]); + + let raw_from = FilAddress::new_delegated(10 as ActorID, &[0x22; 20]) + .context("failed to create delegated sender address")?; + let raw_to = FilAddress::new_delegated(10 as ActorID, &[0x33; 20]) + .context("failed to create delegated receiver address")?; + + let from = IPCAddress::new(&subnet_id, &raw_from)?; + let to = IPCAddress::new(&subnet_id, &raw_to)?; + + let api_env = ApiIpcEnvelope { + kind: IpcMsgKind::Transfer, + to, + value: TokenAmount::from_atto(0), + from, + message: vec![0xAA, 0xBB], + local_nonce: 1, + original_nonce: 2, + }; + + let evm_env = lib_gateway::IpcEnvelope::try_from(api_env.clone()) + .context("failed to convert api IpcEnvelope to evm IpcEnvelope")?; + + // Build event topics and data matching the proofs generator format. + let subnet_eth = EthAddress::from_slice(&[0x11; 20]); + let topic_subnet_bytes = encode(&[Token::Address(subnet_eth)]); + let topic_subnet = H256::from_slice(&topic_subnet_bytes); + + let id = [0x42u8; 32]; + let topic_id_bytes = encode(&[Token::FixedBytes(id.to_vec())]); + let topic_id = H256::from_slice(&topic_id_bytes); + + let topic0 = lib_gateway::NewTopDownMessageFilter::signature(); + let data_bytes = encode(&[evm_env.clone().into_token()]); + + let proof = mk_event_proof( + vec![ + h256_to_0x_string(topic0), + h256_to_0x_string(topic_subnet), + h256_to_0x_string(topic_id), + ], + bytes_to_0x_string(&data_bytes), + ); + + let bundle = UnifiedProofBundle { + storage_proofs: vec![], + event_proofs: vec![proof], + blocks: vec![], + }; + + let out = extract_topdown_messages(&bundle)?; + assert_eq!(out.len(), 1); + assert_eq!(out[0], api_env); + Ok(()) + } + + #[test] + fn extracts_and_decodes_new_power_change_request_event() -> Result<()> { + let validator_eth = EthAddress::from_slice(&[0x77; 20]); + let payload = vec![0xDE, 0xAD, 0xBE, 0xEF]; + let configuration_number = 42u64; + + let topic0 = lib_power_change_log::NewPowerChangeRequestFilter::signature(); + let data_bytes = encode(&[ + Token::Uint(U256::from(1u8)), // PowerOperation::SetPower + Token::Address(validator_eth), + Token::Bytes(payload.clone()), + Token::Uint(U256::from(configuration_number)), + ]); + + let proof = mk_event_proof( + vec![h256_to_0x_string(topic0)], + bytes_to_0x_string(&data_bytes), + ); + + let bundle = UnifiedProofBundle { + storage_proofs: vec![], + event_proofs: vec![proof], + blocks: vec![], + }; + + let out = extract_validator_changes(&bundle)?; + assert_eq!(out.len(), 1); + assert_eq!(out[0].configuration_number, configuration_number); + + let expected_validator = ethers_address_to_fil_address(&validator_eth)?; + assert_eq!(out[0].change.validator, expected_validator); + assert_eq!(out[0].change.payload, payload); + + Ok(()) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/f3_topdown.rs b/fendermint/vm/interpreter/src/fvm/f3_topdown.rs new file mode 100644 index 0000000000..bca309e4c6 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/f3_topdown.rs @@ -0,0 +1,579 @@ +// Copyright 2022-2024 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use anyhow::{bail, Context}; +use fendermint_vm_message::chain::ChainMessage; +use fendermint_vm_message::ipc::GeneralisedTopDown; +use fvm_ipld_blockstore::Blockstore; +use ipc_api::cross::IpcEnvelope; +use ipc_api::staking::PowerChangeRequest; +use std::sync::Arc; + +use fendermint_vm_message::ipc::IpcMessage; +use fendermint_vm_topdown_proof_service::types::SerializableF3Certificate; +use fendermint_vm_topdown_proof_service::PowerEntries; + +use crate::fvm::event_extraction::{extract_topdown_messages, extract_validator_changes}; +use crate::fvm::state::ipc::F3LightClientCaller; +use crate::fvm::state::FvmExecState; + +/// F3 finality handler - handles all F3 proof-based finality logic +/// This module encapsulates all F3-specific concerns, keeping TopDownManager clean +#[derive(Clone)] +pub struct F3TopDownHandler { + /// Proof cache for F3-based parent finality (off-chain, local). + proof_cache: Arc, + /// F3 Light Client **actor** caller (on-chain state in the FVM). + f3_light_client_actor_caller: F3LightClientCaller, +} + +impl F3TopDownHandler { + pub fn new(proof_cache: Arc) -> Self { + Self { + proof_cache, + f3_light_client_actor_caller: F3LightClientCaller::new(), + } + } + + /// Get reference to the proof cache + pub fn proof_cache(&self) -> &Arc { + &self.proof_cache + } + + /// Query proof cache for next uncommitted proof and create a chain message with proof bundle. + /// + /// This is the v2 proof-based approach that replaces voting with cryptographic verification. + /// + /// Returns `None` if: + /// - No proof available for next height + /// - Cache is temporarily empty (graceful degradation) + pub fn chain_message_from_proof_cache(&self) -> Option { + // Get next uncommitted proof (epoch after last_committed) + let epoch_with_cert = self.proof_cache.get_next_uncommitted_epoch_with_cert()?; + + tracing::debug!( + instance_id = epoch_with_cert.certificate.gpbft_instance, + epoch = epoch_with_cert.epoch, + "found next uncommitted epoch with certificate in cache" + ); + + // Convert FinalityCertificate to SerializableF3Certificate for message + let serializable_cert = SerializableF3Certificate::from(&epoch_with_cert.certificate); + + Some(ChainMessage::Ipc(IpcMessage::GeneralisedTopDown( + GeneralisedTopDown { + height: epoch_with_cert.epoch, + certificate: fendermint_vm_message::ipc::Certificate::FilecoinF3(serializable_cert), + }, + ))) + } + + /// Attest a generalised top-down message during the attestation phase. + /// + /// Cache-first attestation. + /// + /// We require that: + /// - there is an epoch proof in the local cache for `msg.height` + /// - the certificate attached to the message matches the certificate referenced by the cache entry + /// + /// Proof bundle validity is verified at proof generation time (before insertion into the cache). + pub async fn attest( + &self, + state: &mut FvmExecState, + msg: &GeneralisedTopDown, + ) -> anyhow::Result<()> + where + DB: Blockstore + Clone + 'static + Send + Sync, + { + let msg_cert = match &msg.certificate { + fendermint_vm_message::ipc::Certificate::FilecoinF3(cert) => cert, + }; + + let cached = self + .proof_cache + .get_epoch_proof_with_certificate(msg.height) + .ok_or_else(|| { + anyhow::anyhow!( + "proof bundle not found in local cache for height {}", + msg.height + ) + })?; + + let cached_cert = SerializableF3Certificate::from(&cached.certificate); + if &cached_cert != msg_cert { + bail!( + "certificate mismatch for epoch {} (message instance {}, cache instance {})", + msg.height, + msg_cert.gpbft_instance, + cached_cert.gpbft_instance + ); + } + + // Check on-chain continuity (this needs actor state access, hence in attestation). + let f3_state = self.get_f3_light_client_actor_state(state)?; + let instance_id = cached.certificate.gpbft_instance; + + // Certificate instance must not go backwards; it either stays the same (multiple epochs can + // be proven under the same certificate) or advances by exactly 1. + if instance_id < f3_state.latest_instance_id { + bail!( + "certificate instance went backwards: {} < {}", + instance_id, + f3_state.latest_instance_id + ); + } + if instance_id > f3_state.latest_instance_id + 1 { + bail!( + "certificate instance jumped: {} > {}", + instance_id, + f3_state.latest_instance_id + 1 + ); + } + + // Epoch must advance by exactly 1 relative to the latest finalized epoch in state. + // + // At genesis this is `None` (no finality yet). In that case we skip the check here; the + // cache lookup (and later execution) will still enforce that we only process epochs we + // have proofs for. + if let Some(prev_finalized) = f3_state.latest_finalized_height { + if msg.height != prev_finalized + 1 { + bail!( + "epoch is not sequential: message height {} != expected {}", + msg.height, + prev_finalized + 1 + ); + } + } + + Ok(()) + } + + /// Execute F3-specific logic for a generalised top-down message. + /// Returns the topdown messages and validator changes to be processed by TopDownManager. + pub fn extract_messages_and_validator_changes( + &self, + _state: &mut FvmExecState, + msg: &GeneralisedTopDown, + ) -> anyhow::Result<(Vec, Vec, u64)> + where + DB: Blockstore + Clone + 'static + Send + Sync, + { + // Cache is the source of truth: get the proof + certificate for this epoch. + let cached = self + .proof_cache + .get_epoch_proof_with_certificate(msg.height) + .ok_or_else(|| { + anyhow::anyhow!( + "proof bundle not found in local cache for height {}", + msg.height + ) + })?; + + // We don't validate the message certificate here; that happens during attestation. + let instance_id = cached.certificate.gpbft_instance; + + tracing::debug!( + instance = instance_id, + height = msg.height, + "executing F3 generalised top-down" + ); + + let msgs = extract_topdown_messages(&cached.proof_bundle)?; + let validator_changes = extract_validator_changes(&cached.proof_bundle)?; + + tracing::debug!( + message_count = msgs.len(), + validator_changes_count = validator_changes.len(), + "extracted topdown effects from proof bundle" + ); + + Ok((msgs, validator_changes, instance_id)) + } + + /// Finalize F3 execution after all top-down effects have been applied successfully. + /// + /// This updates the on-chain F3 light client state and marks the epoch as committed in the proof cache. + pub fn finalize_after_execution( + &self, + state: &mut FvmExecState, + epoch: fvm_shared::clock::ChainEpoch, + instance_id: u64, + ) -> anyhow::Result<()> + where + DB: Blockstore + Clone + 'static + Send + Sync, + { + // Update F3LightClientActor with new certificate state (on-chain). + let power_table = ActorPowerTable::from(&self.get_power_table(instance_id)?).0; + let new_light_client_state = + fendermint_vm_actor_interface::f3_light_client::LightClientState { + latest_instance_id: instance_id, + latest_finalized_height: Some(epoch), + power_table, + }; + + self.update_f3_light_client_actor_state(state, new_light_client_state)?; + tracing::debug!(instance = instance_id, "updated F3LightClientActor state"); + + // Mark epoch as committed in cache. + if let Err(e) = self.mark_committed(epoch, instance_id) { + tracing::warn!( + error = %e, + epoch, + instance = instance_id, + "failed to mark epoch as committed in cache" + ); + } else { + tracing::debug!( + epoch, + instance = instance_id, + "marked epoch as committed in cache" + ); + } + + Ok(()) + } + + /// Get power table for a certificate instance from the **cache** (off-chain). + fn get_power_table(&self, instance_id: u64) -> anyhow::Result { + let cert_entry = self + .proof_cache + .get_certificate(instance_id) + .ok_or_else(|| { + anyhow::anyhow!( + "certificate not found in cache for instance {}", + instance_id + ) + })?; + + Ok(cert_entry.power_table) + } + + /// Mark epoch as committed in the **cache** (off-chain). + fn mark_committed( + &self, + epoch: fvm_shared::clock::ChainEpoch, + instance_id: u64, + ) -> anyhow::Result<()> { + self.proof_cache + .mark_committed(epoch, instance_id) + .map_err(|e| { + anyhow::anyhow!( + "failed to mark epoch {} as committed in cache: {}", + epoch, + e + ) + }) + } + + /// Get F3 Light Client **actor** state (on-chain). + fn get_f3_light_client_actor_state( + &self, + state: &mut FvmExecState, + ) -> anyhow::Result + where + DB: Blockstore + Clone + 'static + Send + Sync, + { + self.f3_light_client_actor_caller + .get_state(state) + .context("failed to get F3LightClientActor state") + } + + /// Update F3 Light Client **actor** state (on-chain). + fn update_f3_light_client_actor_state( + &self, + state: &mut FvmExecState, + new_state: fendermint_vm_actor_interface::f3_light_client::LightClientState, + ) -> anyhow::Result<()> + where + DB: Blockstore + Clone + 'static + Send + Sync, + { + self.f3_light_client_actor_caller + .update_state(state, new_state) + .context("failed to update F3LightClientActor state") + } +} + +/// Local helper newtype so we can provide a clean `From` impl at the conversion boundary. +struct ActorPowerTable(Vec); + +impl From<&PowerEntries> for ActorPowerTable { + fn from(entries: &PowerEntries) -> Self { + Self( + entries + .iter() + .map(|pe| { + // Convert BigInt -> u64 (saturating if too large). + // Power should be non-negative; we ignore the sign here and keep the magnitude. + let (_sign, digits) = pe.power.to_u64_digits(); + let power = if digits.is_empty() { + 0 + } else if digits.len() == 1 { + digits[0] + } else { + u64::MAX + }; + + fendermint_vm_actor_interface::f3_light_client::PowerEntry { + id: pe.id, + public_key: pe.pub_key.0.clone(), + power, + } + }) + .collect(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::F3TopDownHandler; + use crate::fvm::state::FvmGenesisState; + use crate::fvm::store::memory::MemoryBlockstore; + use anyhow::Context; + use cid::multihash::Multihash; + use fendermint_vm_actor_interface::{f3_light_client, gas_market, init, system}; + use fendermint_vm_core::Timestamp; + use fendermint_vm_genesis::PowerScale; + use fendermint_vm_message::chain::ChainMessage; + use fendermint_vm_message::ipc::{Certificate, IpcMessage}; + use fendermint_vm_topdown_proof_service::config::CacheConfig; + use fendermint_vm_topdown_proof_service::types::{ + CertificateEntry, EpochProofEntry, SerializableCertificateEntry, SerializableECChainEntry, + SerializableF3Certificate, SerializablePowerEntries, SerializablePowerEntry, + SerializableSupplementalData, + }; + use fendermint_vm_topdown_proof_service::ProofCache; + use fvm::engine::MultiEngine; + use fvm_shared::clock::ChainEpoch; + use fvm_shared::econ::TokenAmount; + use fvm_shared::version::NetworkVersion; + use num_traits::Zero; + use proofs::proofs::common::bundle::UnifiedProofBundle; + use std::collections::BTreeSet; + use std::sync::Arc; + + fn mk_test_certificate_entry(instance_id: u64, epochs: Vec) -> CertificateEntry { + let mh = Multihash::<64>::wrap(0x12, &[0u8; 32]).expect("valid multihash"); + let power_table_cid = cid::Cid::new_v1(0x55, mh).to_string(); + + let ec_chain = epochs + .into_iter() + .map(|epoch| SerializableECChainEntry { + epoch, + key: vec!["0".to_string()], + power_table: power_table_cid.clone(), + commitments: vec![0u8; 32], + }) + .collect(); + + let serializable = SerializableCertificateEntry { + certificate: SerializableF3Certificate { + gpbft_instance: instance_id, + ec_chain, + supplemental_data: SerializableSupplementalData { + power_table: power_table_cid.clone(), + commitments: vec![0u8; 32], + }, + signers: vec![0], + signature: vec![], + power_table_delta: vec![], + }, + power_table: SerializablePowerEntries(vec![ + SerializablePowerEntry { + id: 1, + power: "1000".to_string(), + pub_key: vec![1u8; 48], + }, + SerializablePowerEntry { + id: 2, + power: "2000".to_string(), + pub_key: vec![2u8; 48], + }, + ]), + source_rpc: "test".to_string(), + fetched_at: std::time::SystemTime::now(), + }; + + CertificateEntry::try_from(serializable).expect("valid certificate entry") + } + + #[tokio::test] + async fn f3_topdown_handler_end_to_end_cache_to_finalize() -> anyhow::Result<()> { + // Minimal FVM genesis state with F3LightClientActor so attestation can query actor state. + let store = MemoryBlockstore::new(); + let multi_engine = Arc::new(MultiEngine::new(1)); + let mut genesis_state = FvmGenesisState::new( + store, + multi_engine, + actors_builtin_car::CAR, + actors_custom_car::CAR, + ) + .await + .context("failed to create FVM genesis state")?; + + // System actor (required so the FVM can load the builtin actor manifest). + genesis_state + .create_builtin_actor( + system::SYSTEM_ACTOR_CODE_ID, + system::SYSTEM_ACTOR_ID, + &system::State { + builtin_actors: genesis_state.manifest_data_cid, + }, + TokenAmount::zero(), + None, + ) + .context("failed to create system actor")?; + + // Init actor (safe default for message execution environment). + let (init_state, _addr_to_id) = init::State::new( + genesis_state.store(), + "test".to_string(), + &[], + &BTreeSet::new(), + 0, + ) + .context("failed to create init state")?; + genesis_state + .create_builtin_actor( + init::INIT_ACTOR_CODE_ID, + init::INIT_ACTOR_ID, + &init_state, + TokenAmount::zero(), + None, + ) + .context("failed to create init actor")?; + + // Gas market custom actor: required by BlockGasTracker initialization. + let gas_market_state = fendermint_actor_gas_market_eip1559::State { + base_fee: TokenAmount::from_atto(100), + constants: fendermint_actor_gas_market_eip1559::Constants::default(), + }; + genesis_state + .create_custom_actor( + fendermint_actor_gas_market_eip1559::ACTOR_NAME, + gas_market::GAS_MARKET_ACTOR_ID, + &gas_market_state, + TokenAmount::zero(), + None, + ) + .context("failed to create gas market actor")?; + + let instance_id = 7u64; + let base_epoch: ChainEpoch = 50; + let genesis_power_table = vec![f3_light_client::PowerEntry { + id: 10, + public_key: vec![9u8; 48], + power: 9, + }]; + + let f3_state = fendermint_actor_f3_light_client::state::State::new( + instance_id, + Some(base_epoch), + genesis_power_table, + ) + .context("failed to create F3 light client actor state")?; + genesis_state + .create_custom_actor( + fendermint_actor_f3_light_client::F3_LIGHT_CLIENT_ACTOR_NAME, + f3_light_client::F3_LIGHT_CLIENT_ACTOR_ID, + &f3_state, + TokenAmount::zero(), + None, + ) + .context("failed to create F3 light client actor")?; + + // Initialize execution params (required for executing implicit/read-only messages). + genesis_state + .init_exec_state( + Timestamp(1), + NetworkVersion::V21, + TokenAmount::from_atto(100), + TokenAmount::zero(), + 1, + 0 as PowerScale, + ) + .context("failed to init exec state")?; + let mut exec_state = genesis_state + .into_exec_state() + .map_err(|_| anyhow::anyhow!("genesis exec state missing"))?; + + // Prepare a cache with exactly one next epoch proof. + let cache = ProofCache::new( + base_epoch, + instance_id, + CacheConfig { + lookahead_instances: 10, + retention_epochs: 10, + }, + ); + cache + .insert_certificate(mk_test_certificate_entry( + instance_id, + vec![base_epoch, base_epoch + 1], + )) + .context("failed to insert certificate")?; + cache + .insert_epoch_proofs(vec![EpochProofEntry::new( + base_epoch + 1, + UnifiedProofBundle { + storage_proofs: vec![], + event_proofs: vec![], + blocks: vec![], + }, + instance_id, + )]) + .context("failed to insert epoch proof")?; + + let handler = F3TopDownHandler::new(Arc::new(cache.clone())); + + // Propose from cache. + let chain_msg = handler + .chain_message_from_proof_cache() + .expect("next uncommitted epoch proof exists"); + let msg = match chain_msg { + ChainMessage::Ipc(IpcMessage::GeneralisedTopDown(m)) => m, + other => anyhow::bail!("unexpected chain message: {other:?}"), + }; + assert_eq!(msg.height, base_epoch + 1); + + // Attest: cache match + on-chain continuity. + handler + .attest(&mut exec_state, &msg) + .await + .context("attestation failed")?; + + // Extract effects (should be empty in this fabricated proof bundle). + let (topdown_msgs, validator_changes, used_instance) = + handler.extract_messages_and_validator_changes(&mut exec_state, &msg)?; + assert!(topdown_msgs.is_empty()); + assert!(validator_changes.is_empty()); + assert_eq!(used_instance, instance_id); + + // Finalize: updates actor state + marks cache committed. + handler + .finalize_after_execution(&mut exec_state, msg.height, used_instance) + .context("finalize failed")?; + + // Actor state updated. + let caller = crate::fvm::state::ipc::F3LightClientCaller::new(); + let actor_state = caller.get_state(&mut exec_state)?; + assert_eq!(actor_state.latest_instance_id, instance_id); + assert_eq!(actor_state.latest_finalized_height, Some(base_epoch + 1)); + assert_eq!(actor_state.power_table.len(), 2); + assert_eq!(actor_state.power_table[0].id, 1); + assert_eq!(actor_state.power_table[0].power, 1000); + + // Cache committed cursor updated. + assert_eq!( + handler.proof_cache().last_committed(), + (base_epoch + 1, instance_id) + ); + + // Sanity: message certificate is FilecoinF3 (we don't decode internals here). + match msg.certificate { + Certificate::FilecoinF3(_) => {} + } + + Ok(()) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/interpreter.rs b/fendermint/vm/interpreter/src/fvm/interpreter.rs index 5a3cb5bc52..c36b1377fd 100644 --- a/fendermint/vm/interpreter/src/fvm/interpreter.rs +++ b/fendermint/vm/interpreter/src/fvm/interpreter.rs @@ -275,9 +275,10 @@ where .into_iter() .map(Into::into); + // Get parent finality message - TopDownManager decides internally whether to use F3 or legacy let top_down_iter = self .top_down_manager - .chain_message_from_finality_or_quorum() + .chain_message_for_proposal() .await .into_iter(); @@ -317,7 +318,7 @@ where async fn attest_block_messages( &self, - state: FvmExecState>>, + mut state: FvmExecState>>, msgs: Vec>, ) -> Result { if msgs.len() > self.max_msgs_per_block { @@ -329,17 +330,43 @@ where } let mut block_gas_usage = 0; - let base_fee = state.block_gas_tracker().base_fee(); + // Clone to avoid holding an immutable borrow of `state` while we also need mutable access + // during top-down attestation. + let base_fee = state.block_gas_tracker().base_fee().clone(); for msg in msgs { match fvm_ipld_encoding::from_slice::(&msg) { Ok(chain_msg) => match chain_msg { + ChainMessage::Ipc(IpcMessage::GeneralisedTopDown(ref msg)) => { + // Attest generalised top-down message (checks local cache + on-chain continuity). + match self + .top_down_manager + .attest_generalised(&mut state, msg) + .await + { + Ok(()) => { + tracing::debug!( + height = msg.height, + "generalised top-down message attested successfully" + ); + } + Err(e) => { + tracing::warn!( + error = %e, + height = msg.height, + "proof bundle verification failed - rejecting block" + ); + return Ok(AttestMessagesResponse::Reject); + } + } + } ChainMessage::Ipc(IpcMessage::TopDownExec(finality)) => { - if !self.top_down_manager.is_finality_valid(finality).await { + // v1 voting-based finality (kept for backward compatibility) + if !self.top_down_manager.attest_legacy(finality).await { return Ok(AttestMessagesResponse::Reject); } } ChainMessage::Signed(signed) => { - if signed.message.gas_fee_cap < *base_fee { + if signed.message.gas_fee_cap < base_fee { tracing::warn!( fee_cap = signed.message.gas_fee_cap.to_string(), base_fee = base_fee.to_string(), @@ -459,9 +486,19 @@ where }) } ChainMessage::Ipc(ipc_msg) => match ipc_msg { + IpcMessage::GeneralisedTopDown(msg) => { + let applied_message = self + .top_down_manager + .execute_generalised(state, msg) + .await?; + Ok(ApplyMessageResponse { + applied_message, + domain_hash: None, + }) + } IpcMessage::TopDownExec(p) => { - let applied_message = - self.top_down_manager.execute_topdown_msg(state, p).await?; + // OLD: v1 voting-based execution (kept for backward compatibility) + let applied_message = self.top_down_manager.execute_legacy(state, p).await?; Ok(ApplyMessageResponse { applied_message, domain_hash: None, diff --git a/fendermint/vm/interpreter/src/fvm/legacy_topdown.rs b/fendermint/vm/interpreter/src/fvm/legacy_topdown.rs new file mode 100644 index 0000000000..8275032079 --- /dev/null +++ b/fendermint/vm/interpreter/src/fvm/legacy_topdown.rs @@ -0,0 +1,145 @@ +// Copyright 2022-2026 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use async_stm::atomically; +use fendermint_tracing::emit; +use fendermint_vm_event::ParentFinalityMissingQuorum; +use fendermint_vm_message::chain::ChainMessage; +use fendermint_vm_message::ipc::{IpcMessage, ParentFinality}; +use fendermint_vm_topdown::proxy::IPCProviderProxyWithLatency; +use fendermint_vm_topdown::voting::ValidatorKey; +use fendermint_vm_topdown::voting::VoteTally; +use fendermint_vm_topdown::{ + BlockHeight, CachedFinalityProvider, IPCParentFinality, ParentFinalityProvider, + ParentViewProvider, Toggle, +}; +use fvm_shared::clock::ChainEpoch; +use std::sync::Arc; + +use crate::fvm::end_block_hook::PowerUpdates; + +type TopDownFinalityProvider = Arc>>; + +/// Legacy (vote-based) parent finality handler. +/// +/// Encapsulates all vote/quorum/provider logic; `TopDownManager` should orchestrate execution. +#[derive(Clone)] +pub struct LegacyTopDownHandler { + provider: TopDownFinalityProvider, + votes: VoteTally, +} + +impl LegacyTopDownHandler { + pub fn new(provider: TopDownFinalityProvider, votes: VoteTally) -> Self { + Self { provider, votes } + } + + pub fn is_enabled(&self) -> bool { + self.provider.is_enabled() + } + + pub fn genesis_epoch(&self) -> anyhow::Result { + self.provider.genesis_epoch() + } + + pub async fn attest(&self, finality: ParentFinality) -> bool { + let prop = IPCParentFinality { + height: finality.height as u64, + block_hash: finality.block_hash, + }; + atomically(|| self.provider.check_proposal(&prop)).await + } + + pub async fn update_voting_power_table(&self, power_updates: &PowerUpdates) { + let power_updates_mapped: Vec<_> = power_updates + .0 + .iter() + .map(|v| (ValidatorKey::from(v.public_key.0), v.power.0)) + .collect(); + + atomically(|| self.votes.update_power_table(power_updates_mapped.clone())).await + } + + pub async fn chain_message_for_proposal(&self) -> Option { + tracing::debug!("using legacy voting-based finality"); + self.chain_message_from_finality_or_quorum().await + } + + pub async fn validator_changes_from( + &self, + from: BlockHeight, + to: BlockHeight, + ) -> anyhow::Result> { + self.provider.validator_changes_from(from, to).await + } + + pub async fn top_down_msgs_from( + &self, + from: BlockHeight, + to: BlockHeight, + ) -> anyhow::Result> { + self.provider.top_down_msgs_from(from, to).await + } + + pub async fn on_finality_executed( + &self, + finality: IPCParentFinality, + proposer: Option<&str>, + local_block_height: u64, + ) -> anyhow::Result<()> { + atomically(|| { + self.provider.set_new_finality(finality.clone())?; + self.votes.set_finalized( + finality.height, + finality.block_hash.clone(), + proposer, + Some(local_block_height), + )?; + Ok(()) + }) + .await; + Ok(()) + } + + async fn chain_message_from_finality_or_quorum(&self) -> Option { + atomically(|| self.votes.pause_votes_until_find_quorum()).await; + + let (parent, quorum) = atomically(|| { + let parent = self.provider.next_proposal()?; + + let quorum = self + .votes + .find_quorum()? + .map(|(height, block_hash)| IPCParentFinality { height, block_hash }); + + Ok((parent, quorum)) + }) + .await; + + let parent = parent?; + + let quorum = if let Some(quorum) = quorum { + quorum + } else { + emit!( + DEBUG, + ParentFinalityMissingQuorum { + block_height: parent.height, + block_hash: &hex::encode(&parent.block_hash), + } + ); + return None; + }; + + let finality = if parent.height <= quorum.height { + parent + } else { + quorum + }; + + Some(ChainMessage::Ipc(IpcMessage::TopDownExec(ParentFinality { + height: finality.height as ChainEpoch, + block_hash: finality.block_hash, + }))) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/mod.rs b/fendermint/vm/interpreter/src/fvm/mod.rs index 762c8b696a..70adf2a704 100644 --- a/fendermint/vm/interpreter/src/fvm/mod.rs +++ b/fendermint/vm/interpreter/src/fvm/mod.rs @@ -4,11 +4,15 @@ pub mod constants; mod executions; mod externs; +pub mod f3_topdown; pub mod interpreter; +pub mod legacy_topdown; pub mod observe; pub mod state; pub mod store; pub mod topdown; +pub use f3_topdown::F3TopDownHandler; +pub use legacy_topdown::LegacyTopDownHandler; pub mod upgrades; pub use interpreter::FvmMessagesInterpreter; @@ -17,6 +21,7 @@ pub mod bundle; pub mod activity; pub mod end_block_hook; +pub mod event_extraction; pub(crate) mod gas; pub(crate) mod gas_estimation; diff --git a/fendermint/vm/interpreter/src/fvm/state/ipc.rs b/fendermint/vm/interpreter/src/fvm/state/ipc.rs index 52f55dde81..024af188a7 100644 --- a/fendermint/vm/interpreter/src/fvm/state/ipc.rs +++ b/fendermint/vm/interpreter/src/fvm/state/ipc.rs @@ -1,16 +1,17 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use anyhow::Context; +use anyhow::{bail, Context}; use fvm_ipld_blockstore::Blockstore; use fvm_shared::econ::TokenAmount; use fvm_shared::ActorID; +use num_traits::Zero; use fendermint_crypto::PublicKey; use fendermint_vm_actor_interface::ipc; use fendermint_vm_actor_interface::{ - eam::EthAddress, init::builtin_actor_eth_addr, ipc::GATEWAY_ACTOR_ID, + eam::EthAddress, f3_light_client, init::builtin_actor_eth_addr, ipc::GATEWAY_ACTOR_ID, system, }; use fendermint_vm_genesis::{Collateral, Power, PowerScale, Validator, ValidatorKey}; use fendermint_vm_message::conv::from_eth; @@ -297,3 +298,271 @@ fn membership_to_power_table( pt } + +/// Caller for the F3 Light Client actor +/// +/// This actor is responsible for: +/// - Storing finalized F3 instance state (instance ID, finalized epochs, validator power table) +/// - Validator power table +#[derive(Clone)] +pub struct F3LightClientCaller { + actor_id: ActorID, +} + +impl F3LightClientCaller { + pub fn new() -> Self { + Self { + actor_id: f3_light_client::F3_LIGHT_CLIENT_ACTOR_ID, + } + } + + /// Update the F3 light client state after verifying a proof bundle. + /// + /// This should be called after successfully executing a proof-based topdown finality message. + pub fn update_state( + &self, + state: &mut FvmExecState, + light_client_state: f3_light_client::LightClientState, + ) -> anyhow::Result<()> { + let method_num = f3_light_client::Method::UpdateState as u64; + + let params = f3_light_client::UpdateStateParams { + state: light_client_state, + }; + + let params_bytes = + fvm_ipld_encoding::to_vec(¶ms).context("failed to serialize update params")?; + + let msg = fvm_shared::message::Message { + version: Default::default(), + from: fvm_shared::address::Address::new_id(system::SYSTEM_ACTOR_ID), + to: fvm_shared::address::Address::new_id(self.actor_id), + sequence: 0, + value: TokenAmount::zero(), + method_num, + params: fvm_ipld_encoding::RawBytes::new(params_bytes), + gas_limit: 10_000_000_000, + gas_fee_cap: TokenAmount::zero(), + gas_premium: TokenAmount::zero(), + }; + + let (ret, _) = state + .execute_implicit(msg) + .context("failed to execute F3 light client update")?; + + if let Some(err) = &ret.failure_info { + bail!( + "F3 light client update failed (exit code {}): {}", + ret.msg_receipt.exit_code.value(), + err + ); + } + + Ok(()) + } + + /// Get the current F3 instance state from the light client actor. + pub fn get_state( + &self, + state: &mut FvmExecState, + ) -> anyhow::Result { + let method_num = f3_light_client::Method::GetState as u64; + + let msg = fvm_shared::message::Message { + version: Default::default(), + from: fvm_shared::address::Address::new_id(system::SYSTEM_ACTOR_ID), + to: fvm_shared::address::Address::new_id(self.actor_id), + sequence: 0, + value: TokenAmount::zero(), + method_num, + params: fvm_ipld_encoding::RawBytes::default(), + gas_limit: 10_000_000_000, + gas_fee_cap: TokenAmount::zero(), + gas_premium: TokenAmount::zero(), + }; + + // Read-only execution still requires `&mut FvmExecState` (it uses interior caches), but + // any effects are reverted by the executor. + let (ret, _) = state + .execute_read_only(msg) + .context("failed to execute F3 light client get_state")?; + + if let Some(err) = &ret.failure_info { + bail!( + "F3 light client get_state failed (exit code {}): {}", + ret.msg_receipt.exit_code.value(), + err + ); + } + + let state_response: f3_light_client::GetStateResponse = + fvm_ipld_encoding::from_slice(&ret.msg_receipt.return_data.bytes()) + .context("failed to deserialize F3 light client state")?; + + Ok(state_response) + } +} + +#[cfg(test)] +mod tests { + use super::F3LightClientCaller; + use crate::fvm::state::genesis::FvmGenesisState; + use crate::fvm::store::memory::MemoryBlockstore; + use anyhow::Context; + use fendermint_vm_actor_interface::{f3_light_client, gas_market, init, system}; + use fendermint_vm_core::Timestamp; + use fendermint_vm_genesis::PowerScale; + use fvm::engine::MultiEngine; + use fvm_shared::clock::ChainEpoch; + use fvm_shared::econ::TokenAmount; + use fvm_shared::version::NetworkVersion; + use num_traits::Zero; + use std::collections::BTreeSet; + use std::sync::Arc; + + #[tokio::test] + async fn f3_light_client_caller_roundtrip_update_and_get_state() -> anyhow::Result<()> { + // Build a minimal genesis state with the built-in bundle, plus the custom F3 actor. + let store = MemoryBlockstore::new(); + let multi_engine = Arc::new(MultiEngine::new(1)); + let mut genesis_state = FvmGenesisState::new( + store, + multi_engine, + actors_builtin_car::CAR, + actors_custom_car::CAR, + ) + .await + .context("failed to create FVM genesis state")?; + + // System actor (required so the FVM can load the builtin actor manifest). + genesis_state + .create_builtin_actor( + system::SYSTEM_ACTOR_CODE_ID, + system::SYSTEM_ACTOR_ID, + &system::State { + builtin_actors: genesis_state.manifest_data_cid, + }, + TokenAmount::zero(), + None, + ) + .context("failed to create system actor")?; + + // Init actor (safe default for message execution environment). + let (init_state, _addr_to_id) = init::State::new( + genesis_state.store(), + "test".to_string(), + &[], + &BTreeSet::new(), + 0, + ) + .context("failed to create init state")?; + genesis_state + .create_builtin_actor( + init::INIT_ACTOR_CODE_ID, + init::INIT_ACTOR_ID, + &init_state, + TokenAmount::zero(), + None, + ) + .context("failed to create init actor")?; + + // Gas market custom actor: required by BlockGasTracker initialization. + let gas_market_state = fendermint_actor_gas_market_eip1559::State { + base_fee: TokenAmount::from_atto(100), + constants: fendermint_actor_gas_market_eip1559::Constants::default(), + }; + genesis_state + .create_custom_actor( + fendermint_actor_gas_market_eip1559::ACTOR_NAME, + gas_market::GAS_MARKET_ACTOR_ID, + &gas_market_state, + TokenAmount::zero(), + None, + ) + .context("failed to create gas market actor")?; + + // Create the F3 light client custom actor. + let instance_id = 10u64; + let base_epoch: ChainEpoch = 1234; + let power_table = vec![ + fendermint_actor_f3_light_client::types::PowerEntry { + id: 1, + public_key: vec![1, 2, 3], + power: 100, + }, + fendermint_actor_f3_light_client::types::PowerEntry { + id: 2, + public_key: vec![4, 5, 6], + power: 200, + }, + ]; + let f3_state = fendermint_actor_f3_light_client::state::State::new( + instance_id, + Some(base_epoch), + power_table.clone(), + ) + .context("failed to create F3 light client actor state")?; + genesis_state + .create_custom_actor( + fendermint_actor_f3_light_client::F3_LIGHT_CLIENT_ACTOR_NAME, + f3_light_client::F3_LIGHT_CLIENT_ACTOR_ID, + &f3_state, + TokenAmount::zero(), + None, + ) + .context("failed to create F3 light client actor")?; + + // Initialize execution params (required for executing implicit/read-only messages). + genesis_state + .init_exec_state( + Timestamp(1), + NetworkVersion::V21, + TokenAmount::from_atto(100), + TokenAmount::zero(), + 1, + 0 as PowerScale, + ) + .context("failed to init exec state")?; + + let mut exec_state = genesis_state + .into_exec_state() + .map_err(|_| anyhow::anyhow!("genesis exec state missing"))?; + + let caller = F3LightClientCaller::new(); + + // Round-trip: read initial actor state. + let state0 = caller.get_state(&mut exec_state)?; + assert_eq!(state0.latest_instance_id, instance_id); + assert_eq!(state0.latest_finalized_height, Some(base_epoch)); + assert_eq!(state0.power_table.len(), power_table.len()); + assert_eq!(state0.power_table[0].id, 1); + + // Update state and read again. + let new_state = f3_light_client::LightClientState { + latest_instance_id: instance_id + 1, + latest_finalized_height: Some(base_epoch + 1), + power_table: vec![f3_light_client::PowerEntry { + id: 99, + public_key: vec![9u8; 48], + power: 999, + }], + }; + caller + .update_state(&mut exec_state, new_state.clone()) + .context("failed to update F3LightClientActor state")?; + + let state1 = caller.get_state(&mut exec_state)?; + assert_eq!(state1.latest_instance_id, new_state.latest_instance_id); + assert_eq!( + state1.latest_finalized_height, + new_state.latest_finalized_height + ); + assert_eq!(state1.power_table, new_state.power_table); + + // Also sanity-check that read-only exec doesn't mutate the actor (it reverts effects). + let state2 = caller.get_state(&mut exec_state)?; + assert_eq!(state2, state1); + + Ok(()) + } +} diff --git a/fendermint/vm/interpreter/src/fvm/state/query.rs b/fendermint/vm/interpreter/src/fvm/state/query.rs index e555bcdd91..547dd5f2ce 100644 --- a/fendermint/vm/interpreter/src/fvm/state/query.rs +++ b/fendermint/vm/interpreter/src/fvm/state/query.rs @@ -108,7 +108,7 @@ where } /// If we know the query is over the state, cache the state tree. - async fn with_exec_state(self, f: F) -> anyhow::Result<(Self, T)> + pub async fn with_exec_state(self, f: F) -> anyhow::Result<(Self, T)> where F: FnOnce(&mut FvmExecState>) -> anyhow::Result, { diff --git a/fendermint/vm/interpreter/src/fvm/topdown.rs b/fendermint/vm/interpreter/src/fvm/topdown.rs index 4fb6c9a6c9..d201585295 100644 --- a/fendermint/vm/interpreter/src/fvm/topdown.rs +++ b/fendermint/vm/interpreter/src/fvm/topdown.rs @@ -1,21 +1,9 @@ // Copyright 2022-2024 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use async_stm::atomically; -use fendermint_tracing::emit; -use fendermint_vm_event::ParentFinalityMissingQuorum; use fendermint_vm_message::chain::ChainMessage; -use fendermint_vm_message::ipc::IpcMessage; use fendermint_vm_message::ipc::ParentFinality; -use fendermint_vm_topdown::proxy::IPCProviderProxyWithLatency; -use fendermint_vm_topdown::voting::ValidatorKey; -use fendermint_vm_topdown::voting::VoteTally; -use fendermint_vm_topdown::{ - BlockHeight, CachedFinalityProvider, IPCParentFinality, ParentFinalityProvider, - ParentViewProvider, Toggle, -}; -use fvm_shared::clock::ChainEpoch; -use std::sync::Arc; +use fendermint_vm_topdown::{BlockHeight, IPCParentFinality}; use crate::fvm::state::ipc::GatewayCaller; use crate::fvm::state::FvmExecState; @@ -23,19 +11,25 @@ use anyhow::{bail, Context}; use fvm_ipld_blockstore::Blockstore; use crate::fvm::end_block_hook::PowerUpdates; +use crate::fvm::f3_topdown::F3TopDownHandler; +use crate::fvm::legacy_topdown::LegacyTopDownHandler; use crate::fvm::state::ipc::tokens_to_mint; use crate::types::AppliedMessage; use ipc_api::cross::IpcEnvelope; -type TopDownFinalityProvider = Arc>>; +#[derive(Clone)] +pub enum TopDownFinalityHandler { + Disabled, + Legacy(LegacyTopDownHandler), + F3(F3TopDownHandler), +} #[derive(Clone)] pub struct TopDownManager where DB: Blockstore + Clone + 'static + Send + Sync, { - provider: TopDownFinalityProvider, - votes: VoteTally, + finality: TopDownFinalityHandler, // Gateway caller for IPC gateway interactions gateway_caller: GatewayCaller, } @@ -44,95 +38,144 @@ impl TopDownManager where DB: Blockstore + Clone + 'static + Send + Sync, { - pub fn new(provider: TopDownFinalityProvider, votes: VoteTally) -> Self { + pub fn new(finality: TopDownFinalityHandler) -> Self { Self { - provider, - votes, + finality, gateway_caller: GatewayCaller::default(), } } - pub async fn is_finality_valid(&self, finality: ParentFinality) -> bool { - let prop = IPCParentFinality { - height: finality.height as u64, - block_hash: finality.block_hash, - }; - atomically(|| self.provider.check_proposal(&prop)).await + pub fn disabled() -> Self { + Self::new(TopDownFinalityHandler::Disabled) } - /// Prepares a top-down execution message based on the current parent's finality proposal and quorum. - /// - /// This function first pauses incoming votes to prevent interference during processing. It then atomically retrieves - /// both the next parent's proposal and the quorum of votes. If either the parent's proposal or the quorum is missing, - /// the function returns `None`. When both are available, it selects the finality with the lower block height and wraps - /// it into a `ChainMessage` for top-down execution. - pub async fn chain_message_from_finality_or_quorum(&self) -> Option { - // Prepare top down proposals. - // Before we try to find a quorum, pause incoming votes. This is optional but if there are lots of votes coming in it might hold up proposals. - atomically(|| self.votes.pause_votes_until_find_quorum()).await; - - // The pre-requisite for proposal is that there is a quorum of gossiped votes at that height. - // The final proposal can be at most as high as the quorum, but can be less if we have already, - // hit some limits such as how many blocks we can propose in a single step. - let (parent, quorum) = atomically(|| { - let parent = self.provider.next_proposal()?; - - let quorum = self - .votes - .find_quorum()? - .map(|(height, block_hash)| IPCParentFinality { height, block_hash }); - - Ok((parent, quorum)) - }) - .await; - - // If there is no parent proposal, exit early. - let parent = parent?; - - // Require a quorum; if it's missing, log and exit. - let quorum = if let Some(quorum) = quorum { - quorum - } else { - emit!( - DEBUG, - ParentFinalityMissingQuorum { - block_height: parent.height, - block_hash: &hex::encode(&parent.block_hash), - } - ); - return None; - }; + pub fn legacy(handler: LegacyTopDownHandler) -> Self { + Self::new(TopDownFinalityHandler::Legacy(handler)) + } - // Choose the lower height between the parent's proposal and the quorum. - let finality = if parent.height <= quorum.height { - parent - } else { - quorum - }; + pub fn f3(handler: F3TopDownHandler) -> Self { + Self::new(TopDownFinalityHandler::F3(handler)) + } + + pub async fn attest_legacy(&self, finality: ParentFinality) -> bool { + match &self.finality { + TopDownFinalityHandler::Legacy(h) => h.attest(finality).await, + TopDownFinalityHandler::F3(_) | TopDownFinalityHandler::Disabled => false, + } + } - Some(ChainMessage::Ipc(IpcMessage::TopDownExec(ParentFinality { - height: finality.height as ChainEpoch, - block_hash: finality.block_hash, - }))) + /// Get the chain message for parent finality proposal. + /// + /// This method encapsulates the decision of which finality mechanism to use: + /// - If configured for legacy: use legacy voting-based finality + /// - If configured for F3: use F3 proof-based finality (no fallback) + /// + /// The caller doesn't need to know which mechanism is being used. + pub async fn chain_message_for_proposal(&self) -> Option { + match &self.finality { + TopDownFinalityHandler::Disabled => None, + TopDownFinalityHandler::Legacy(h) => h.chain_message_for_proposal().await, + TopDownFinalityHandler::F3(f3) => { + let proof_msg = f3.chain_message_from_proof_cache()?; + tracing::debug!("using F3 proof-based parent finality in proposal"); + Some(proof_msg) + } + } + } + + /// Attest a generalised top-down message during the attestation phase. + /// + /// Delegates to F3 handler if F3 is configured, otherwise returns error. + pub async fn attest_generalised( + &self, + state: &mut FvmExecState, + msg: &fendermint_vm_message::ipc::GeneralisedTopDown, + ) -> anyhow::Result<()> + where + BS: Blockstore + Clone + 'static + Send + Sync, + { + match &self.finality { + TopDownFinalityHandler::F3(f3) => f3.attest(state, msg).await, + TopDownFinalityHandler::Legacy(_) | TopDownFinalityHandler::Disabled => Err( + anyhow::anyhow!("F3 not configured - cannot attest generalised top-down message"), + ), + } } pub async fn update_voting_power_table(&self, power_updates: &PowerUpdates) { - let power_updates_mapped: Vec<_> = power_updates - .0 - .iter() - .map(|v| (ValidatorKey::from(v.public_key.0), v.power.0)) - .collect(); + if let TopDownFinalityHandler::Legacy(h) = &self.finality { + h.update_voting_power_table(power_updates).await + } + } + + /// Execute generalised top-down message. + /// Delegates F3-specific logic to F3 module, handles common top-down execution. + pub async fn execute_generalised( + &self, + state: &mut FvmExecState, + msg: fendermint_vm_message::ipc::GeneralisedTopDown, + ) -> anyhow::Result { + let f3 = match &self.finality { + TopDownFinalityHandler::F3(f3) => f3, + TopDownFinalityHandler::Legacy(_) | TopDownFinalityHandler::Disabled => { + bail!("F3 not configured - cannot execute without F3 handler") + } + }; + + // Execute F3-specific logic (certificate validation, proof extraction, state updates) + let (msgs, validator_changes, instance_id) = + f3.extract_messages_and_validator_changes(state, &msg)?; + + // Commit parent finality to gateway + let finality = IPCParentFinality::new(msg.height as i64, vec![]); + let (prev_height, _prev_finality) = self + .commit_finality(state, finality.clone(), 0) + .await + .context("failed to commit finality")?; + + tracing::debug!( + previous_height = prev_height, + current_height = finality.height, + "committed parent finality" + ); + + // Store validator changes in gateway + self.gateway_caller + .store_validator_changes(state, validator_changes) + .context("failed to store validator changes")?; - atomically(|| self.votes.update_power_table(power_updates_mapped.clone())).await + // Execute topdown messages + let ret = self + .execute_topdown_msgs(state, msgs) + .await + .context("failed to execute top down messages")?; + + // Finalize F3 execution only after all effects were applied successfully. + f3.finalize_after_execution(state, msg.height, instance_id) + .context("failed to finalize F3 execution")?; + + tracing::info!( + height = msg.height, + "generalised top-down executed successfully" + ); + + Ok(ret) } // TODO Karel - separate this huge function and clean up - pub async fn execute_topdown_msg( + pub async fn execute_legacy( &self, state: &mut FvmExecState, finality: ParentFinality, ) -> anyhow::Result { - if !self.provider.is_enabled() { + let legacy = match &self.finality { + TopDownFinalityHandler::Legacy(h) => h, + TopDownFinalityHandler::F3(_) => bail!("cannot execute legacy top-down: F3 enabled"), + TopDownFinalityHandler::Disabled => { + bail!("cannot execute IPC top-down message: parent provider disabled") + } + }; + if !legacy.is_enabled() { bail!("cannot execute IPC top-down message: parent provider disabled"); } @@ -144,7 +187,7 @@ where ); let (prev_height, prev_finality) = self - .commit_finality(state, finality.clone()) + .commit_finality(state, finality.clone(), legacy.genesis_epoch()?) .await .context("failed to commit finality")?; @@ -170,8 +213,7 @@ where let (execution_fr, execution_to) = (prev_height + 1, finality.height); // error happens if we cannot get the validator set from ipc agent after retries - let validator_changes = self - .provider + let validator_changes = legacy .validator_changes_from(execution_fr, execution_to) .await .context("failed to fetch validator changes")?; @@ -188,8 +230,7 @@ where .context("failed to store validator changes")?; // error happens if we cannot get the cross messages from ipc agent after retries - let msgs = self - .provider + let msgs = legacy .top_down_msgs_from(execution_fr, execution_to) .await .context("failed to fetch top down messages")?; @@ -214,19 +255,10 @@ where .map(|id| hex::encode(id.serialize_compressed())); let proposer_ref = proposer.as_deref(); - atomically(|| { - self.provider.set_new_finality(finality.clone())?; - - self.votes.set_finalized( - finality.height, - finality.block_hash.clone(), - proposer_ref, - Some(local_block_height), - )?; - - Ok(()) - }) - .await; + legacy + .on_finality_executed(finality.clone(), proposer_ref, local_block_height) + .await + .context("failed to record new finality")?; tracing::debug!( finality = finality.to_string(), @@ -242,6 +274,7 @@ where &self, state: &mut FvmExecState, finality: IPCParentFinality, + genesis_epoch: BlockHeight, ) -> anyhow::Result<(BlockHeight, Option)> { let (prev_height, prev_finality) = if let Some(prev_finality) = self .gateway_caller @@ -249,7 +282,7 @@ where { (prev_finality.height, Some(prev_finality)) } else { - (self.provider.genesis_epoch()?, None) + (genesis_epoch, None) }; tracing::debug!( diff --git a/fendermint/vm/interpreter/src/genesis.rs b/fendermint/vm/interpreter/src/genesis.rs index 581c75d492..0d1d044136 100644 --- a/fendermint/vm/interpreter/src/genesis.rs +++ b/fendermint/vm/interpreter/src/genesis.rs @@ -444,17 +444,12 @@ impl<'a> GenesisBuilder<'a> { // F3 Light Client actor - manages F3 light client state for proof-based parent finality if let Some(f3_params) = &genesis.f3 { - // For subnets with F3 parameters, initialize with the provided F3 data - // Note: finalized_epochs always starts empty at genesis - let constructor_params = fendermint_actor_f3_light_client::types::ConstructorParams { - instance_id: f3_params.instance_id, - power_table: f3_params.power_table.clone(), - finalized_epochs: Vec::new(), - }; + // We treat the ECChain base epoch for the configured instance as already finalized + // by the previous certificate. The node will start proving/executing from base_epoch + 1. let f3_state = fendermint_actor_f3_light_client::state::State::new( - constructor_params.instance_id, - constructor_params.power_table, - constructor_params.finalized_epochs, + f3_params.instance_id, + Some(f3_params.base_epoch), + f3_params.power_table.clone(), )?; state diff --git a/fendermint/vm/message/Cargo.toml b/fendermint/vm/message/Cargo.toml index 34459becbb..93dd32e033 100644 --- a/fendermint/vm/message/Cargo.toml +++ b/fendermint/vm/message/Cargo.toml @@ -30,7 +30,9 @@ ipc-api = { path = "../../../ipc/api" } fendermint_crypto = { path = "../../crypto" } fendermint_vm_encoding = { path = "../encoding" } fendermint_vm_actor_interface = { path = "../actor_interface" } +fendermint_vm_topdown_proof_service = { path = "../topdown/proof-service" } fendermint_testing = { path = "../../testing", optional = true } +proofs = { git = "https://github.com/consensus-shipyard/ipc-filecoin-proofs.git", branch = "proofs" } [dev-dependencies] ethers = { workspace = true } diff --git a/fendermint/vm/message/src/ipc.rs b/fendermint/vm/message/src/ipc.rs index 8f275a1c24..c20ec567aa 100644 --- a/fendermint/vm/message/src/ipc.rs +++ b/fendermint/vm/message/src/ipc.rs @@ -8,9 +8,10 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[allow(clippy::large_enum_variant)] pub enum IpcMessage { - /// A top-down checkpoint parent finality proposal. This proposal should contain the latest parent - /// state that to be checked and voted by validators. + /// A top-down checkpoint parent finality proposal (legacy voting-based) TopDownExec(ParentFinality), + /// Generalized top-down finality with extensible certificate types + GeneralisedTopDown(GeneralisedTopDown), } /// A proposal of the parent view that validators will be voting on. @@ -22,6 +23,23 @@ pub struct ParentFinality { pub block_hash: Vec, } +/// Generalized top-down finality structure +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct GeneralisedTopDown { + /// The chain epoch this finality is for (height) + pub height: ChainEpoch, + /// The certificate that certifies finality (type-specific, proof is fetched from local cache) + pub certificate: Certificate, +} + +/// Certificate types (extensible for future certificate types) +/// Each variant contains the certificate data. Proofs are fetched from local cache when needed. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum Certificate { + /// Filecoin F3 certificate (proof bundle is fetched from local cache using instance ID) + FilecoinF3(fendermint_vm_topdown_proof_service::types::SerializableF3Certificate), +} + #[cfg(feature = "arb")] mod arb { diff --git a/fendermint/vm/topdown/proof-service/Cargo.toml b/fendermint/vm/topdown/proof-service/Cargo.toml index b5ba35a98d..0e891d7301 100644 --- a/fendermint/vm/topdown/proof-service/Cargo.toml +++ b/fendermint/vm/topdown/proof-service/Cargo.toml @@ -31,7 +31,6 @@ fendermint_actor_f3_light_client = { path = "../../../actors/f3-light-client" } fendermint_vm_genesis = { path = "../../genesis" } # IPC -ipc-provider = { path = "../../../../ipc/provider" } ipc-api = { path = "../../../../ipc/api" } ipc-observability = { path = "../../../../ipc/observability" } diff --git a/fendermint/vm/topdown/proof-service/README.md b/fendermint/vm/topdown/proof-service/README.md index 79750d1d39..6cc8ff77df 100644 --- a/fendermint/vm/topdown/proof-service/README.md +++ b/fendermint/vm/topdown/proof-service/README.md @@ -196,7 +196,8 @@ let config = ProofServiceConfig { retention_epochs: 100, }, polling_interval: Duration::from_secs(30), - ..Default::default() + max_cache_size_bytes: 100 * 1024 * 1024, // 100 MB + fallback_rpc_urls: vec![], }; // Launch service with optional persistence @@ -411,9 +412,9 @@ Older issue with reqwest library on macOS (now fixed in upstream). ### Unit Tests -```bash -cargo test --package fendermint_vm_topdown_proof_service --lib -``` +````bash +# Unit tests +cargo test --package fendermint_vm_topdown_proof_service **Test Coverage:** @@ -424,10 +425,41 @@ cargo test --package fendermint_vm_topdown_proof_service --lib - Metrics registration ### Integration Tests - ```bash # Requires live Calibration network cargo test --package fendermint_vm_topdown_proof_service --test integration -- --ignored +```` + +### End-to-End Testing + +1. **Deploy Test Contract** (optional - for testing with TopdownMessenger): + +```bash +cd /path/to/proofs/topdown-messenger +forge create --rpc-url http://api.calibration.node.glif.io/rpc/v1 \ + --private-key $PRIVATE_KEY \ + src/TopdownMessenger.sol:TopdownMessenger +``` + +2. **Run Proof Service**: + +```bash +./target/debug/proof-cache-test run \ + --rpc-url "http://api.calibration.node.glif.io/rpc/v1" \ + --initial-instance \ + --gateway-actor-id \ + --subnet-id "your-subnet-id" \ + --poll-interval 10 \ + --lookahead 3 \ + --db-path /tmp/proof-cache-test.db +``` + +3. **Inspect Results**: + +```bash +# After stopping the service +./target/debug/proof-cache-test inspect --db-path /tmp/proof-cache-test.db +./target/debug/proof-cache-test get --db-path /tmp/proof-cache-test.db --instance-id ``` ### End-to-End Testing diff --git a/fendermint/vm/topdown/proof-service/src/bootstrap.rs b/fendermint/vm/topdown/proof-service/src/bootstrap.rs new file mode 100644 index 0000000000..062af12d06 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/bootstrap.rs @@ -0,0 +1,44 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Bootstrap helpers for integrating the proof service with on-chain state. + +use crate::config; +use crate::PowerEntries; +use anyhow::{Context, Result}; +use ipc_api::subnet_id::SubnetID; + +/// Fetch an F3 certificate for a specific instance from the parent chain. +/// +/// Uses the F3 light client RPC (not the Lotus JSON-RPC wrapper), which supports fetching +/// certificates by instance ID. +pub async fn fetch_certificate( + parent_rpc_url: &str, + subnet_id: &SubnetID, + instance_id: u64, +) -> Result { + let network = config::f3_network_name(subnet_id); + let light_client = filecoin_f3_lightclient::LightClient::new(parent_rpc_url, &network) + .context("failed to create F3 light client")?; + light_client + .get_certificate(instance_id) + .await + .context("failed to fetch F3 certificate by instance") +} + +/// Convert the on-chain F3LightClientActor power table into GPBFT `PowerEntries`. +/// +/// This preserves participant IDs, which are required for certificate verification. +pub fn power_entries_from_actor( + entries: &[fendermint_actor_f3_light_client::types::PowerEntry], +) -> PowerEntries { + PowerEntries( + entries + .iter() + .map(|e| filecoin_f3_gpbft::PowerEntry { + id: e.id, + power: num_bigint::BigInt::from(e.power), + pub_key: filecoin_f3_gpbft::PubKey(e.public_key.clone()), + }) + .collect(), + ) +} diff --git a/fendermint/vm/topdown/proof-service/src/cache.rs b/fendermint/vm/topdown/proof-service/src/cache.rs index f088baf294..db7a1cfc35 100644 --- a/fendermint/vm/topdown/proof-service/src/cache.rs +++ b/fendermint/vm/topdown/proof-service/src/cache.rs @@ -260,6 +260,24 @@ impl ProofCache { self.last_committed_instance.load(Ordering::Acquire) } + /// Get the next uncommitted epoch (last_committed_epoch + 1) + /// Returns None if no proof is available for that epoch + pub fn get_next_uncommitted_epoch(&self) -> Option { + let next_epoch = self.last_committed_epoch() + 1; + if self.contains_epoch_proof(next_epoch) { + Some(next_epoch) + } else { + None + } + } + + /// Get the next uncommitted proof entry (epoch + certificate) + /// Returns None if no proof is available for next epoch + pub fn get_next_uncommitted_epoch_with_cert(&self) -> Option { + let next_epoch = self.get_next_uncommitted_epoch()?; + self.get_epoch_proof_with_certificate(next_epoch) + } + /// Get the number of cached epoch proofs pub fn epoch_proof_count(&self) -> usize { self.epoch_proofs.read().len() diff --git a/fendermint/vm/topdown/proof-service/src/config.rs b/fendermint/vm/topdown/proof-service/src/config.rs index c2820d2975..6b0e2dd305 100644 --- a/fendermint/vm/topdown/proof-service/src/config.rs +++ b/fendermint/vm/topdown/proof-service/src/config.rs @@ -9,6 +9,25 @@ use std::time::Duration; const FILECOIN_MAINNET_CHAIN_ID: u64 = 314; const FILECOIN_CALIBRATION_CHAIN_ID: u64 = 314159; +/// Derive the F3 network name from the subnet root chain ID. +/// +/// This is used for interacting with the Filecoin F3 RPC. +pub fn f3_network_name(subnet_id: &SubnetID) -> String { + let root_id = subnet_id.root_id(); + + match root_id { + FILECOIN_MAINNET_CHAIN_ID => "mainnet".to_string(), + FILECOIN_CALIBRATION_CHAIN_ID => "calibrationnet".to_string(), + _ => { + tracing::warn!( + root_id, + "Unknown root chain ID for F3, defaulting to calibrationnet" + ); + "calibrationnet".to_string() + } + } +} + /// Represents a value that can be either a numeric Actor ID or an Ethereum address string. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -65,19 +84,7 @@ impl ProofServiceConfig { } pub fn f3_network_name(&self, subnet_id: &SubnetID) -> String { - let root_id = subnet_id.root_id(); - - match root_id { - FILECOIN_MAINNET_CHAIN_ID => "mainnet".to_string(), - FILECOIN_CALIBRATION_CHAIN_ID => "calibrationnet".to_string(), - _ => { - tracing::warn!( - root_id, - "Unknown root chain ID for F3, defaulting to calibrationnet" - ); - "calibrationnet".to_string() - } - } + f3_network_name(subnet_id) } } diff --git a/fendermint/vm/topdown/proof-service/src/lib.rs b/fendermint/vm/topdown/proof-service/src/lib.rs index cc8c51bb87..e566e25b51 100644 --- a/fendermint/vm/topdown/proof-service/src/lib.rs +++ b/fendermint/vm/topdown/proof-service/src/lib.rs @@ -18,6 +18,7 @@ //! the same certificate. pub mod assembler; +pub mod bootstrap; pub mod cache; pub mod config; pub mod f3_client; @@ -28,8 +29,10 @@ pub mod types; pub mod verifier; // Re-export main types for convenience +pub use bootstrap::{fetch_certificate, power_entries_from_actor}; pub use cache::ProofCache; pub use config::{CacheConfig, ProofServiceConfig}; +pub use filecoin_f3_gpbft::PowerEntries; pub use service::ProofGeneratorService; pub use types::{ CertificateEntry, EpochProofEntry, EpochProofWithCertificate, SerializableF3Certificate, diff --git a/fendermint/vm/topdown/proof-service/src/persistence.rs b/fendermint/vm/topdown/proof-service/src/persistence.rs index b61c086f4f..e348000113 100644 --- a/fendermint/vm/topdown/proof-service/src/persistence.rs +++ b/fendermint/vm/topdown/proof-service/src/persistence.rs @@ -181,14 +181,6 @@ impl ProofCachePersistence { self.clear_all() } - /// Load the last committed instance ID - /// - /// Note: This information is not persisted to disk, so this always returns None. - /// The last committed state is only stored in memory in the ProofCache. - pub fn load_last_committed(&self) -> Result> { - Ok(None) - } - /// Load all entries as combined cache entries /// /// This combines certificates with their associated epoch proofs for inspection. diff --git a/fendermint/vm/topdown/proof-service/src/service.rs b/fendermint/vm/topdown/proof-service/src/service.rs index 88271f31fa..e074864dfa 100644 --- a/fendermint/vm/topdown/proof-service/src/service.rs +++ b/fendermint/vm/topdown/proof-service/src/service.rs @@ -19,7 +19,8 @@ use crate::assembler::ProofAssembler; use crate::cache::ProofCache; use crate::config::{GatewayId, ProofServiceConfig}; use crate::f3_client::F3Client; -use crate::types::{CertificateEntry, EpochProofEntry, FinalizedTipset}; +use crate::types::{CertificateEntry, EpochProofEntry, FinalizedTipset, FinalizedTipsets}; +use crate::verifier::ProofVerifier; use anyhow::{Context, Result}; use filecoin_f3_certs::FinalityCertificate; use filecoin_f3_gpbft::PowerEntries; @@ -33,6 +34,7 @@ pub struct ProofGeneratorService { cache: Arc, f3_client: F3Client, assembler: ProofAssembler, + verifier: ProofVerifier, } impl ProofGeneratorService { @@ -100,6 +102,7 @@ impl ProofGeneratorService { cache, f3_client, assembler, + verifier: ProofVerifier::new(subnet_id.to_string()), }) } @@ -242,6 +245,15 @@ impl ProofGeneratorService { "Generating proofs for certificate epochs" ); + // The last tipset in the certificate has no child tipset inside this certificate, so it + // cannot be proven yet. We only treat the epochs we generated proofs for as "finalized + // tipsets" for verification purposes. + let finalized_tipsets = { + let parents: Vec = + tipset_pairs.iter().map(|(p, _)| p.clone()).collect(); + FinalizedTipsets::from(parents.as_slice()) + }; + let mut epoch_proofs = Vec::with_capacity(tipset_pairs.len()); // Generate proofs for each (parent, child) pair @@ -261,6 +273,10 @@ impl ProofGeneratorService { .await .with_context(|| format!("Failed to generate proof for epoch {}", parent_epoch))?; + self.verifier + .verify_proof_bundle_with_tipsets(&proof_bundle, &finalized_tipsets) + .with_context(|| format!("Failed to verify proof for epoch {}", parent_epoch))?; + epoch_proofs.push(EpochProofEntry::new( parent_epoch, proof_bundle, diff --git a/fendermint/vm/topdown/proof-service/src/types.rs b/fendermint/vm/topdown/proof-service/src/types.rs index de43e9d4c8..6ccd627481 100644 --- a/fendermint/vm/topdown/proof-service/src/types.rs +++ b/fendermint/vm/topdown/proof-service/src/types.rs @@ -73,6 +73,13 @@ impl From<&ECChain> for FinalizedTipsets { } } +impl From<&[FinalizedTipset]> for FinalizedTipsets { + /// Convert from slice of already-converted `FinalizedTipset`s. + fn from(tipsets: &[FinalizedTipset]) -> Self { + Self(tipsets.to_vec()) + } +} + impl TryFrom<&[proofs::client::types::ApiTipset]> for FinalizedTipsets { type Error = anyhow::Error; diff --git a/ipc/provider/Cargo.toml b/ipc/provider/Cargo.toml index f2c19911bd..50eef07548 100644 --- a/ipc/provider/Cargo.toml +++ b/ipc/provider/Cargo.toml @@ -61,7 +61,6 @@ fendermint_rpc = { path = "../../fendermint/rpc" } fendermint_actor_f3_light_client = { path = "../../fendermint/actors/f3-light-client" } fendermint_vm_genesis = { path = "../../fendermint/vm/genesis" } - [dev-dependencies] tempfile = { workspace = true } hex = { workspace = true }