diff --git a/.github/workflows/contracts-sast.yaml b/.github/workflows/contracts-sast.yaml index 3be9a8d5e2..5573fbd0d7 100644 --- a/.github/workflows/contracts-sast.yaml +++ b/.github/workflows/contracts-sast.yaml @@ -40,7 +40,7 @@ jobs: version: v0.3.0 - name: Install aderyn - run: cargo install aderyn --locked + run: npm install -g @cyfrin/aderyn - name: Make deps run: cd contracts && make deps diff --git a/Cargo.lock b/Cargo.lock index 6e6a6ee419..57e186d6f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,7 +121,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array 0.14.9", ] @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -287,6 +287,15 @@ 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" @@ -312,7 +321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "cpufeatures", "password-hash 0.5.0", ] @@ -374,7 +383,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] @@ -386,7 +395,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -563,7 +572,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -651,7 +660,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -743,7 +752,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -847,6 +856,12 @@ dependencies = [ "match-lookup", ] +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.13.1" @@ -910,7 +925,7 @@ dependencies = [ "bellpepper-core", "bincode", "blake2s_simd 1.0.3", - "blstrs 0.7.1", + "blstrs", "byteorder", "crossbeam-channel", "digest 0.10.7", @@ -920,7 +935,7 @@ dependencies = [ "group 0.13.0", "log", "memmap2", - "pairing 0.23.0", + "pairing", "rand 0.8.5", "rand_core 0.6.4", "rayon", @@ -957,7 +972,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -966,7 +981,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -975,7 +990,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1007,11 +1022,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -1044,6 +1059,14 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2" +version = "0.11.0-rc.2" +source = "git+https://github.com/huitseeker/hashes.git?branch=blake2x-pr#4d3debf264a45da9e33d52645eb6ee9963336f66" +dependencies = [ + "digest 0.11.0-rc.3", +] + [[package]] name = "blake2b_simd" version = "1.0.3" @@ -1108,6 +1131,15 @@ dependencies = [ "generic-array 0.14.9", ] +[[package]] +name = "block-buffer" +version = "0.11.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -1139,22 +1171,6 @@ dependencies = [ "bit-vec 0.4.4", ] -[[package]] -name = "bls-signatures" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1659e487883b92123806f16ff3568dd57563991231d187d29b23eea5d910e800" -dependencies = [ - "blst", - "blstrs 0.6.2", - "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", - "rand_core 0.6.4", - "subtle", - "thiserror 1.0.69", -] - [[package]] name = "bls-signatures" version = "0.15.0" @@ -1162,10 +1178,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc7fce0356b52c2483bb6188cc8bdc11add526bce75d1a44e5e5d889a6ab008" dependencies = [ "blst", - "blstrs 0.7.1", + "blstrs", "ff 0.13.1", "group 0.13.0", - "pairing 0.23.0", + "pairing", "rand_core 0.6.4", "subtle", "thiserror 1.0.69", @@ -1183,22 +1199,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "blstrs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff3694b352ece02eb664a09ffb948ee69b35afa2e6ac444a6b8cb9d515deebd" -dependencies = [ - "blst", - "byte-slice-cast", - "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", - "rand_core 0.6.4", - "serde", - "subtle", -] - [[package]] name = "blstrs" version = "0.7.1" @@ -1210,7 +1210,7 @@ dependencies = [ "ec-gpu", "ff 0.13.1", "group 0.13.0", - "pairing 0.23.0", + "pairing", "rand_core 0.6.4", "serde", "subtle", @@ -1240,7 +1240,7 @@ dependencies = [ "serde_urlencoded", "thiserror 1.0.69", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "url", "winapi", ] @@ -1253,7 +1253,7 @@ checksum = "b58071e8fd9ec1e930efd28e3a90c1251015872a2ce49f81f36421b86466932e" dependencies = [ "serde", "serde_repr", - "serde_with 3.15.0", + "serde_with 3.15.1", ] [[package]] @@ -1268,9 +1268,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -1421,9 +1421,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "jobserver", @@ -1431,6 +1431,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1533,7 +1539,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", "zeroize", ] @@ -1566,9 +1572,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -1576,9 +1582,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -1588,11 +1594,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.59" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" +checksum = "8e602857739c5a4291dfa33b5a298aeac9006185229a700e5810a3ef7272d971" dependencies = [ - "clap 4.5.49", + "clap 4.5.51", ] [[package]] @@ -1604,7 +1610,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1734,6 +1740,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1858,6 +1874,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2114,6 +2140,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto-mac" version = "0.8.0" @@ -2185,7 +2220,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2222,7 +2257,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2233,7 +2268,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2272,7 +2307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2311,9 +2346,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", "serde_core", @@ -2327,7 +2362,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2356,7 +2391,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2368,7 +2403,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "unicode-xid", ] @@ -2395,7 +2430,18 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac89f8a64533a9b0eaa73a68e424db0fb1fd6271c74cc0125336a05f090568d" +dependencies = [ + "block-buffer 0.11.0-rc.5", + "crypto-common 0.2.0-rc.4", "subtle", ] @@ -2460,7 +2506,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2680,7 +2726,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2802,7 +2848,7 @@ dependencies = [ "impl-codec", "impl-rlp", "impl-serde", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "uint 0.9.5", ] @@ -2873,7 +2919,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", - "syn 2.0.106", + "syn 2.0.108", "toml 0.8.23", "walkdir", ] @@ -2891,7 +2937,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2916,8 +2962,8 @@ dependencies = [ "rlp 0.5.2", "serde", "serde_json", - "strum", - "syn 2.0.106", + "strum 0.26.3", + "syn 2.0.108", "tempfile", "thiserror 1.0.69", "tiny-keccak", @@ -3091,7 +3137,7 @@ checksum = "3a82608ee96ce76aeab659e9b8d3c2b787bffd223199af88c674923d861ada10" dependencies = [ "execute-command-macro", "execute-command-tokens", - "generic-array 1.3.3", + "generic-array 1.3.5", ] [[package]] @@ -3111,7 +3157,7 @@ checksum = "ce8cd46a041ad005ab9c71263f9a0ff5b529eac0fe4cc9b4a20f4f0765d8cf4b" dependencies = [ "execute-command-tokens", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3333,6 +3379,7 @@ dependencies = [ "fendermint_vm_resolver", "fendermint_vm_snapshot", "fendermint_vm_topdown", + "fendermint_vm_topdown_proof_service", "fs-err", "fvm", "fvm_ipld_blockstore 0.3.1", @@ -3370,7 +3417,7 @@ dependencies = [ "tendermint-proto 0.31.1", "tendermint-rpc", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "toml 0.8.23", "tower 0.4.13", "tower-abci", @@ -3387,7 +3434,7 @@ dependencies = [ "anyhow", "bytes", "cid 0.11.1", - "clap 4.5.49", + "clap 4.5.51", "ethers", "fendermint_materializer", "fendermint_vm_actor_interface", @@ -3490,7 +3537,7 @@ dependencies = [ "async-trait", "axum", "cid 0.11.1", - "clap 4.5.49", + "clap 4.5.51", "erased-serde", "ethers", "ethers-contract", @@ -3611,7 +3658,7 @@ dependencies = [ "tendermint-rpc", "text-tables", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "toml 0.8.23", "tracing", "url", @@ -3643,7 +3690,7 @@ dependencies = [ "base64 0.21.7", "bytes", "cid 0.11.1", - "clap 4.5.49", + "clap 4.5.51", "ethers", "fendermint_crypto", "fendermint_vm_actor_interface", @@ -3782,7 +3829,7 @@ dependencies = [ name = "fendermint_vm_event" version = "0.1.0" dependencies = [ - "strum", + "strum 0.26.3", ] [[package]] @@ -3877,14 +3924,14 @@ dependencies = [ "serde_json", "serde_with 2.3.3", "snap", - "strum", + "strum 0.26.3", "tempfile", "tendermint 0.31.1", "tendermint-rpc", "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tracing", ] @@ -3970,7 +4017,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-stream", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tracing", "unsigned-varint 0.7.2", ] @@ -3985,7 +4032,7 @@ dependencies = [ "async-trait", "bytes", "cid 0.11.1", - "clap 4.5.49", + "clap 4.5.51", "ethers", "fendermint_crypto", "fendermint_testing", @@ -4014,13 +4061,54 @@ dependencies = [ "tracing-subscriber 0.3.20", ] +[[package]] +name = "fendermint_vm_topdown_proof_service" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "chrono", + "cid 0.11.1", + "clap 4.5.51", + "fendermint_actor_f3_light_client", + "fendermint_vm_genesis", + "filecoin-f3-certs", + "filecoin-f3-gpbft", + "filecoin-f3-lightclient", + "filecoin-f3-rpc", + "futures", + "fvm_ipld_bitfield", + "fvm_ipld_encoding 0.5.3", + "fvm_shared", + "humantime-serde", + "ipc-api", + "ipc-observability", + "ipc-provider", + "keccak-hash", + "multihash 0.18.1", + "multihash-codetable", + "num-bigint", + "parking_lot", + "prometheus", + "proofs", + "rocksdb", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", + "url", +] + [[package]] name = "ff" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -4051,7 +4139,7 @@ dependencies = [ "anyhow", "async-std", "cid 0.10.1", - "clap 4.5.49", + "clap 4.5.51", "futures", "fvm_ipld_blockstore 0.2.1", "fvm_ipld_car 0.7.1", @@ -4073,7 +4161,7 @@ dependencies = [ "fvm_ipld_blockstore 0.3.1", "fvm_ipld_encoding 0.5.3", "fvm_shared", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "log", "multihash 0.19.3", "num-derive 0.4.2", @@ -4098,7 +4186,7 @@ dependencies = [ "fvm_ipld_kamt", "fvm_shared", "hex", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "log", "multihash-codetable", "num-derive 0.4.2", @@ -4139,7 +4227,7 @@ dependencies = [ "fvm_sdk", "fvm_shared", "hex", - "integer-encoding 4.0.2", + "integer-encoding 4.1.0", "itertools 0.14.0", "k256 0.13.4", "lazy_static", @@ -4160,15 +4248,102 @@ dependencies = [ "vm_api", ] +[[package]] +name = "filecoin-f3-blssig" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "blake2 0.11.0-rc.2", + "bls-signatures", + "blstrs", + "filecoin-f3-gpbft", + "group 0.13.0", + "hashlink", + "parking_lot", + "rayon", + "thiserror 2.0.17", +] + +[[package]] +name = "filecoin-f3-certs" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "ahash 0.8.12", + "filecoin-f3-gpbft", + "keccak-hash", + "thiserror 2.0.17", +] + +[[package]] +name = "filecoin-f3-gpbft" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "base32", + "cid 0.10.1", + "filecoin-f3-merkle", + "fvm_ipld_bitfield", + "fvm_ipld_encoding 0.5.3", + "getrandom 0.3.4", + "keccak-hash", + "num-bigint", + "num-traits", + "serde", + "serde_cbor", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 2.0.17", +] + +[[package]] +name = "filecoin-f3-lightclient" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "anyhow", + "base64 0.22.1", + "filecoin-f3-blssig", + "filecoin-f3-certs", + "filecoin-f3-gpbft", + "filecoin-f3-rpc", + "hex", + "keccak-hash", + "tokio", +] + +[[package]] +name = "filecoin-f3-merkle" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "anyhow", + "sha3", +] + +[[package]] +name = "filecoin-f3-rpc" +version = "0.1.0" +source = "git+https://github.com/moshababo/rust-f3?branch=cargo-git-compat#f838fcd973e6e7f32298363ceb03a8010a1dc1fe" +dependencies = [ + "anyhow", + "filecoin-f3-gpbft", + "jsonrpsee", + "num-bigint", + "serde", +] + [[package]] name = "filecoin-hashers" -version = "14.0.0" +version = "14.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35146fe3c46db098607ca7decb0349236a90592d6fee0c2eea7301dd1f5733ac" +checksum = "9081144cced0c2b7dc6e7337c2c8c7f4c6ff7ef0bb9c0b75b7f1aaeb1428ebd7" dependencies = [ "anyhow", "bellperson", - "blstrs 0.7.1", + "blstrs", "ff 0.13.1", "generic-array 0.14.9", "hex", @@ -4190,7 +4365,7 @@ dependencies = [ "bellperson", "bincode", "blake2b_simd", - "blstrs 0.7.1", + "blstrs", "ff 0.13.1", "filecoin-hashers", "fr32", @@ -4222,7 +4397,7 @@ checksum = "d50610f79df0975b54461fd65820183b99326fda4f24223d507c1b75cb303b14" dependencies = [ "anyhow", "bincode", - "blstrs 0.7.1", + "blstrs", "filecoin-proofs", "fr32", "lazy_static", @@ -4268,9 +4443,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide 0.8.9", @@ -4343,12 +4518,12 @@ dependencies = [ [[package]] name = "fr32" -version = "12.0.0" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421ea28e99936741d874ac1718a79d5cfdb1a4f3ad6c26950b2386ac94aa3b1a" +checksum = "cf1de08b59372f0316e8c7e304aaec13f180ccb33d55ebe02c10034a0826a2bd" dependencies = [ "anyhow", - "blstrs 0.7.1", + "blstrs", "byte-slice-cast", "byteorder", "ff 0.13.1", @@ -4423,7 +4598,7 @@ dependencies = [ "frc42_hasher 8.0.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4556,7 +4731,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4566,7 +4741,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.32", + "rustls 0.23.34", "rustls-pki-types", ] @@ -4776,7 +4951,7 @@ dependencies = [ "serde", "serde_ipld_dagcbor 0.6.4", "serde_repr", - "serde_tuple 1.1.2", + "serde_tuple 1.1.3", "thiserror 2.0.17", ] @@ -4837,9 +5012,9 @@ source = "git+https://github.com/consensus-shipyard/ref-fvm.git?branch=master#8a dependencies = [ "anyhow", "arbitrary", - "bitflags 2.9.4", + "bitflags 2.10.0", "blake2b_simd", - "bls-signatures 0.15.0", + "bls-signatures", "cid 0.11.1", "data-encoding", "data-encoding-macro", @@ -4889,9 +5064,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.3.3" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42bb3faf529935fbba0684910e1a71ecd271d618549d58f430b878619b7f4cf" +checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ "rustversion", "typenum", @@ -4947,7 +5122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" dependencies = [ "fallible-iterator", - "indexmap 2.11.4", + "indexmap 2.12.0", "stable_deref_trait", ] @@ -4959,9 +5134,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -5001,9 +5176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", - "rand 0.8.5", "rand_core 0.6.4", - "rand_xorshift 0.3.0", "subtle", ] @@ -5032,10 +5205,10 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tracing", ] @@ -5051,13 +5224,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hashbrown" version = "0.12.3" @@ -5100,6 +5279,15 @@ dependencies = [ "fxhash", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -5187,9 +5375,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex-literal" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hex_fmt" @@ -5284,11 +5472,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5371,6 +5559,25 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hybrid-array" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -5477,12 +5684,13 @@ dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.32", + "log", + "rustls 0.23.34", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -5579,9 +5787,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -5592,9 +5800,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -5605,11 +5813,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -5620,42 +5827,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -5707,7 +5910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf9d64cfcf380606e64f9a0bcf493616b65331199f984151a6fa11a7b3cde38" dependencies = [ "async-io 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "fnv", "futures", "if-addrs", @@ -5745,9 +5948,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -5808,7 +6011,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5849,9 +6052,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -5861,9 +6064,12 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "inout" @@ -5892,9 +6098,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integer-encoding" -version = "4.0.2" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d762194228a2f1c11063e46e32e5acb96e66e906382b9eb5441f2e0504bbd5a" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" [[package]] name = "io-lifetimes" @@ -5942,7 +6148,7 @@ dependencies = [ "serde_json", "serde_tuple 0.5.0", "serde_with 2.3.3", - "strum", + "strum 0.26.3", "thiserror 1.0.69", "tracing", ] @@ -5958,7 +6164,7 @@ dependencies = [ "bytes", "chrono", "cid 0.11.1", - "clap 4.5.49", + "clap 4.5.51", "clap_complete", "contracts-artifacts", "env_logger 0.10.2", @@ -6006,14 +6212,14 @@ dependencies = [ "serde_yaml", "sha2 0.10.9", "sha3", - "strum", + "strum 0.26.3", "tar", "tempfile", "tendermint-rpc", "thiserror 1.0.69", "tokio", "tokio-tungstenite 0.18.0", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "toml 0.7.8", "tracing", "tracing-subscriber 0.3.20", @@ -6034,7 +6240,7 @@ dependencies = [ "prometheus", "serde", "serde_with 2.3.3", - "strum", + "strum 0.26.3", "tracing", "tracing-appender", "tracing-subscriber 0.3.20", @@ -6082,7 +6288,7 @@ dependencies = [ "serde_json", "serde_tuple 0.5.0", "serde_with 2.3.3", - "strum", + "strum 0.26.3", "tempfile", "tendermint 0.31.1", "tendermint-rpc", @@ -6133,11 +6339,11 @@ dependencies = [ "argon2", "base64 0.21.7", "blake2b_simd", - "bls-signatures 0.13.1", + "bls-signatures", "ethers", "fs-err", "fvm_shared", - "generic-array 1.3.3", + "generic-array 1.3.5", "hex", "ipc-types", "libc", @@ -6169,7 +6375,7 @@ dependencies = [ "fvm_shared", "lazy_static", "prettyplease", - "syn 2.0.106", + "syn 2.0.108", "thiserror 1.0.69", "tracing", ] @@ -6254,20 +6460,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi 0.5.2", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -6338,6 +6544,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -6350,9 +6578,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -6362,26 +6590,135 @@ dependencies = [ name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonrpc-v2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b2a38e4b2dc33f5bfe487781a5e82d0c60ab2b7f2fa1f132cbac177e9c708" +dependencies = [ + "async-trait", + "bytes", + "erased-serde", + "extensions", + "futures", + "serde", + "serde_json", +] + +[[package]] +name = "jsonrpsee" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-types", + "jsonrpsee-ws-client", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" +dependencies = [ + "base64 0.22.1", + "futures-util", + "http 1.3.1", + "jsonrpsee-core", + "pin-project", + "rustls 0.23.34", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.17", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util 0.7.17", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "jsonrpsee-types", + "pin-project", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" +dependencies = [ + "base64 0.22.1", + "http-body 1.0.1", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls 0.23.34", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" dependencies = [ - "pest", - "pest_derive", + "http 1.3.1", "serde", + "serde_json", + "thiserror 2.0.17", ] [[package]] -name = "jsonrpc-v2" -version = "0.11.0" +name = "jsonrpsee-ws-client" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b2a38e4b2dc33f5bfe487781a5e82d0c60ab2b7f2fa1f132cbac177e9c708" +checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" dependencies = [ - "async-trait", - "bytes", - "erased-serde", - "extensions", - "futures", - "serde", - "serde_json", + "http 1.3.1", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower 0.5.2", + "url", ] [[package]] @@ -6443,6 +6780,16 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-hash" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1b8590eb6148af2ea2d75f38e7d29f5ca970d5a4df456b3ef19b8b415d0264" +dependencies = [ + "primitive-types 0.13.1", + "tiny-keccak", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -6955,7 +7302,7 @@ dependencies = [ "quinn", "rand 0.8.5", "ring 0.17.14", - "rustls 0.23.32", + "rustls 0.23.34", "socket2 0.5.10", "thiserror 1.0.69", "tokio", @@ -7016,7 +7363,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -7049,7 +7396,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.14", - "rustls 0.23.32", + "rustls 0.23.34", "rustls-webpki 0.101.7", "thiserror 1.0.69", "x509-parser", @@ -7093,7 +7440,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -7199,9 +7546,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "literally" @@ -7428,13 +7775,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7493,6 +7840,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfd8a792c1694c6da4f68db0a9d707c72bd260994da179e6030a5dcee00bb815" dependencies = [ "blake2b_simd", + "blake2s_simd 1.0.3", "blake3", "core2", "digest 0.10.7", @@ -7571,7 +7919,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] @@ -7624,7 +7972,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -7638,7 +7986,7 @@ dependencies = [ "bellpepper", "bellpepper-core", "blake2s_simd 0.5.11", - "blstrs 0.7.1", + "blstrs", "byteorder", "ff 0.13.1", "generic-array 0.14.9", @@ -7818,7 +8166,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -7875,9 +8223,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -7885,14 +8233,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -7912,7 +8260,7 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "crc32fast", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.0", "memchr", ] @@ -7933,9 +8281,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -7974,7 +8322,7 @@ version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -7991,7 +8339,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8002,9 +8350,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.3+3.5.4" +version = "300.5.4+3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6bad8cd0233b63971e232cc9c5e83039375b8586d2312f31fda85db8f888c2" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" dependencies = [ "cc", ] @@ -8050,15 +8398,6 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" -[[package]] -name = "pairing" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" -dependencies = [ - "group 0.12.1", -] - [[package]] name = "pairing" version = "0.23.0" @@ -8093,7 +8432,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8292,7 +8631,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8312,7 +8651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.12.0", ] [[package]] @@ -8355,7 +8694,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8384,7 +8723,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8514,9 +8853,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -8569,7 +8908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8586,6 +8925,16 @@ dependencies = [ "uint 0.9.5", ] +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "uint 0.10.0", +] + [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -8631,9 +8980,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -8644,7 +8993,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "hex", "lazy_static", "procfs-core", @@ -8657,7 +9006,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "hex", ] @@ -8698,7 +9047,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8715,14 +9064,46 @@ dependencies = [ "tiny_http", ] +[[package]] +name = "proofs" +version = "0.1.0" +source = "git+https://github.com/consensus-shipyard/ipc-filecoin-proofs?branch=proofs#287aa5d052bb32d191ec0103e6bbb8373f0b3bd3" +dependencies = [ + "anyhow", + "base64 0.21.7", + "cid 0.11.1", + "ethereum-types", + "futures", + "fvm_ipld_amt", + "fvm_ipld_blockstore 0.3.1", + "fvm_ipld_encoding 0.5.3", + "fvm_ipld_hamt", + "fvm_shared", + "hex", + "multihash-codetable", + "parking_lot", + "reqwest 0.11.27", + "serde", + "serde_bytes", + "serde_ipld_dagcbor 0.6.4", + "serde_json", + "serde_tuple 0.5.0", + "sha3", + "thiserror 1.0.69", + "tiny-keccak", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", + "url", +] + [[package]] name = "proptest" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ - "bitflags 2.9.4", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", @@ -8824,10 +9205,11 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "psm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] @@ -8906,7 +9288,7 @@ checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -8924,7 +9306,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.32", + "rustls 0.23.34", "socket2 0.6.1", "thiserror 2.0.17", "tokio", @@ -8944,7 +9326,7 @@ dependencies = [ "rand 0.9.2", "ring 0.17.14", "rustc-hash 2.1.1", - "rustls 0.23.32", + "rustls 0.23.34", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -9128,7 +9510,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -9159,7 +9541,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -9292,7 +9674,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.32", + "rustls 0.23.34", "rustls-pki-types", "serde", "serde_json", @@ -9301,7 +9683,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.26.4", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tower 0.5.2", "tower-http 0.6.6", "tower-service", @@ -9310,7 +9692,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.3", + "webpki-roots 1.0.4", ] [[package]] @@ -9470,7 +9852,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.108", "walkdir", ] @@ -9556,7 +9938,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9569,7 +9951,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -9615,14 +9997,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ + "log", "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] @@ -9636,7 +10019,7 @@ dependencies = [ "openssl-probe", "rustls 0.19.1", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -9648,7 +10031,19 @@ dependencies = [ "openssl-probe", "rustls-pemfile", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", ] [[package]] @@ -9662,14 +10057,41 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.34", + "rustls-native-certs 0.8.2", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.8", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.101.7" @@ -9682,9 +10104,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -9762,7 +10184,7 @@ dependencies = [ "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -9797,9 +10219,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" dependencies = [ "dyn-clone", "ref-cast", @@ -9891,8 +10313,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -9959,6 +10394,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -9976,7 +10421,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10035,7 +10480,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10068,12 +10513,12 @@ dependencies = [ [[package]] name = "serde_tuple" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52569c5296679bd28e2457f067f97d270077df67da0340647da5412c8eac8d9e" +checksum = "6af196b9c06f0aa5555ab980c01a2527b0f67517da8d68b1731b9d4764846a6f" dependencies = [ "serde", - "serde_tuple_macros 1.1.2", + "serde_tuple_macros 1.1.3", ] [[package]] @@ -10089,13 +10534,13 @@ dependencies = [ [[package]] name = "serde_tuple_macros" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f46c707781471741d5f2670edb36476479b26e94cf43efe21ca3c220b97ef2e" +checksum = "ec3a1e7d2eadec84deabd46ae061bf480a91a6bce74d25dad375bd656f2e19d8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10128,17 +10573,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.15.0" +version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.0.5", "serde_core", "serde_json", "time", @@ -10153,7 +10598,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10162,7 +10607,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -10191,7 +10636,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10399,7 +10844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85" dependencies = [ "aes-gcm", - "blake2", + "blake2 0.10.6", "chacha20poly1305", "curve25519-dalek", "rand_core 0.6.4", @@ -10439,6 +10884,21 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + [[package]] name = "solang-parser" version = "0.3.3" @@ -10528,7 +10988,7 @@ dependencies = [ "anyhow", "bellperson", "blake2b_simd", - "blstrs 0.7.1", + "blstrs", "byteorder", "cbc", "config 0.14.1", @@ -10563,7 +11023,7 @@ dependencies = [ "bellperson", "bincode", "blake2b_simd", - "blstrs 0.7.1", + "blstrs", "byte-slice-cast", "byteorder", "chacha20", @@ -10603,7 +11063,7 @@ checksum = "b040787160b2381f1f86ac08f8789283da753e97df25e6be4ea3cc8615d5497c" dependencies = [ "anyhow", "bellperson", - "blstrs 0.7.1", + "blstrs", "byteorder", "ff 0.13.1", "filecoin-hashers", @@ -10623,7 +11083,7 @@ checksum = "1118e3f9dff7c93a68d06a17ae89bf051321278be810e4c3c24a1a88bbc0c3e7" dependencies = [ "anyhow", "bellperson", - "blstrs 0.7.1", + "blstrs", "ff 0.13.1", "filecoin-hashers", "fr32", @@ -10706,7 +11166,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -10719,7 +11188,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.108", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.108", ] [[package]] @@ -10789,9 +11270,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -10833,7 +11314,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -10843,7 +11324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -10853,8 +11334,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -10903,9 +11384,9 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "target-triple" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" @@ -11140,7 +11621,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -11151,7 +11632,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -11227,9 +11708,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -11275,7 +11756,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -11326,7 +11807,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.32", + "rustls 0.23.34", "tokio", ] @@ -11398,9 +11879,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -11449,7 +11930,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -11482,7 +11963,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -11495,7 +11976,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -11509,7 +11990,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", @@ -11551,7 +12032,7 @@ dependencies = [ "rand 0.8.5", "slab", "tokio", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tower-layer", "tower-service", "tracing", @@ -11597,7 +12078,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", @@ -11615,7 +12096,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http 1.3.1", @@ -11671,7 +12152,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -11786,9 +12267,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.112" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d66678374d835fe847e0dc8348fde2ceb5be4a7ec204437d8367f0d8df266a5" +checksum = "559b6a626c0815c942ac98d434746138b4f89ddd6a1b8cbb168c6845fb3376c5" dependencies = [ "glob", "serde", @@ -11910,9 +12391,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -11938,7 +12419,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -12139,7 +12620,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-tungstenite 0.21.0", - "tokio-util 0.7.16", + "tokio-util 0.7.17", "tower-service", "tracing", ] @@ -12161,9 +12642,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -12172,25 +12653,11 @@ 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.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -12201,9 +12668,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -12211,22 +12678,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.108", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -12279,8 +12746,8 @@ version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ - "bitflags 2.9.4", - "indexmap 2.11.4", + "bitflags 2.10.0", + "indexmap 2.12.0", "semver", ] @@ -12290,9 +12757,9 @@ version = "0.226.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc28600dcb2ba68d7e5f1c3ba4195c2bddc918c0243fd702d0b6dbd05689b681" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.0", "semver", "serde", ] @@ -12326,12 +12793,12 @@ checksum = "b9fe78033c72da8741e724d763daf1375c93a38bfcea99c873ee4415f6098c3f" dependencies = [ "addr2line 0.24.2", "anyhow", - "bitflags 2.9.4", + "bitflags 2.10.0", "bumpalo", "cc", "cfg-if", "hashbrown 0.15.5", - "indexmap 2.11.4", + "indexmap 2.12.0", "libc", "log", "mach2", @@ -12406,7 +12873,7 @@ dependencies = [ "cranelift-bitset", "cranelift-entity", "gimli 0.31.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "log", "object 0.36.7", "postcard", @@ -12469,14 +12936,14 @@ checksum = "5732a5c86efce7bca121a61d8c07875f6b85c1607aa86753b40f7f8bd9d3a780" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -12512,6 +12979,24 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.4", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.21.1" @@ -12529,9 +13014,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -12648,7 +13133,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -12659,7 +13144,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -12730,6 +13215,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -12775,6 +13269,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -12823,6 +13332,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -12841,6 +13356,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -12859,6 +13380,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -12889,6 +13416,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -12907,6 +13440,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -12925,6 +13464,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -12943,6 +13488,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -13003,9 +13554,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "ws_stream_wasm" @@ -13076,9 +13627,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmltree" @@ -13174,11 +13725,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -13186,13 +13736,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] @@ -13213,7 +13763,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -13233,7 +13783,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure 0.13.2", ] @@ -13254,14 +13804,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -13270,9 +13820,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -13281,13 +13831,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8a30f3afd3..c5156b3619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "fendermint/testing/*-test", "fendermint/tracing", "fendermint/vm/*", + "fendermint/vm/topdown/proof-service", "fendermint/actors", "fendermint/actors-custom-car", "fendermint/actors-builtin-car", @@ -184,6 +185,8 @@ 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/contracts/contracts/lib/LibGateway.sol b/contracts/contracts/lib/LibGateway.sol index ccc794ab02..0d217c4532 100644 --- a/contracts/contracts/lib/LibGateway.sol +++ b/contracts/contracts/lib/LibGateway.sol @@ -29,6 +29,7 @@ library LibGateway { event MembershipUpdated(Membership); /// @dev subnet refers to the next "down" subnet that the `envelope.message.to` should be forwarded to. + // Keep in sync with the event signature in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEW_TOPDOWN_MESSAGE_SIGNATURE event NewTopDownMessage(address indexed subnet, IpcEnvelope message, bytes32 indexed id); /// @dev event emitted when there is a new bottom-up message added to the batch. /// @dev there is no need to emit the message itself, as the message is included in batch. diff --git a/contracts/contracts/lib/LibGatewayActorStorage.sol b/contracts/contracts/lib/LibGatewayActorStorage.sol index cf6c615cc7..1afe951765 100644 --- a/contracts/contracts/lib/LibGatewayActorStorage.sol +++ b/contracts/contracts/lib/LibGatewayActorStorage.sol @@ -18,6 +18,7 @@ struct GatewayActorStorage { uint64 bottomUpNonce; /// @notice AppliedNonces keep track of the next nonce of the message to be applied. /// This prevents potential replay attacks. + // Keep in sync with the storage slot offset in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:TOPDOWN_NONCE_STORAGE_OFFSET uint64 appliedTopDownNonce; /// @notice Number of active subnets spawned from this one uint64 totalSubnets; diff --git a/contracts/contracts/lib/LibPowerChangeLog.sol b/contracts/contracts/lib/LibPowerChangeLog.sol index ce143567aa..96f9d1cd19 100644 --- a/contracts/contracts/lib/LibPowerChangeLog.sol +++ b/contracts/contracts/lib/LibPowerChangeLog.sol @@ -5,6 +5,7 @@ import {PowerChangeLog, PowerChange, PowerOperation} from "../structs/Subnet.sol /// The util library for `PowerChangeLog` library LibPowerChangeLog { + // Keep in sync with the event signature in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEW_POWER_CHANGE_REQUEST_SIGNATURE event NewPowerChangeRequest(PowerOperation op, address validator, bytes payload, uint64 configurationNumber); /// @notice Validator request to update its metadata diff --git a/contracts/contracts/structs/Subnet.sol b/contracts/contracts/structs/Subnet.sol index ef555d87b7..36240db946 100644 --- a/contracts/contracts/structs/Subnet.sol +++ b/contracts/contracts/structs/Subnet.sol @@ -47,6 +47,7 @@ struct PowerChangeRequest { /// @notice The collection of staking changes. struct PowerChangeLog { /// @notice The next configuration number to assign to new changes. + // Keep in sync with the storage slot offset in the proof-service: fendermint/vm/topdown/proof-service/src/assembler.rs:NEXT_CONFIG_NUMBER_STORAGE_SLOT uint64 nextConfigurationNumber; /// @notice The starting configuration number stored. uint64 startConfigurationNumber; diff --git a/fendermint/app/Cargo.toml b/fendermint/app/Cargo.toml index 33ba6fad21..01c8a95803 100644 --- a/fendermint/app/Cargo.toml +++ b/fendermint/app/Cargo.toml @@ -71,6 +71,7 @@ fendermint_vm_message = { path = "../vm/message" } fendermint_vm_resolver = { path = "../vm/resolver" } 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} diff --git a/fendermint/app/options/src/lib.rs b/fendermint/app/options/src/lib.rs index ac44c2069a..ad146024e7 100644 --- a/fendermint/app/options/src/lib.rs +++ b/fendermint/app/options/src/lib.rs @@ -10,8 +10,8 @@ use fvm_shared::address::Network; use lazy_static::lazy_static; use self::{ - eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, materializer::MaterializerArgs, rpc::RpcArgs, - run::RunArgs, + eth::EthArgs, genesis::GenesisArgs, key::KeyArgs, materializer::MaterializerArgs, + proof_cache::ProofCacheArgs, rpc::RpcArgs, run::RunArgs, }; pub mod config; @@ -20,6 +20,7 @@ pub mod eth; pub mod genesis; pub mod key; pub mod materializer; +pub mod proof_cache; pub mod rpc; pub mod run; @@ -150,6 +151,9 @@ pub enum Commands { /// Subcommands related to the Testnet Materializer. #[clap(aliases = &["mat", "matr", "mate"])] Materializer(MaterializerArgs), + /// Inspect and debug F3 proof cache. + #[clap(name = "proof-cache")] + ProofCache(ProofCacheArgs), } #[cfg(test)] diff --git a/fendermint/app/options/src/proof_cache.rs b/fendermint/app/options/src/proof_cache.rs new file mode 100644 index 0000000000..f259aa2458 --- /dev/null +++ b/fendermint/app/options/src/proof_cache.rs @@ -0,0 +1,44 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use clap::{Args, Subcommand}; +use std::path::PathBuf; + +#[derive(Debug, Args)] +#[command(name = "proof-cache", about = "Inspect and debug F3 proof cache")] +pub struct ProofCacheArgs { + #[command(subcommand)] + pub command: ProofCacheCommands, +} + +#[derive(Debug, Subcommand)] +pub enum ProofCacheCommands { + /// Inspect cache contents + Inspect { + /// Database path + #[arg(long, env = "FM_PROOF_CACHE_DB")] + db_path: PathBuf, + }, + /// Show cache statistics + Stats { + /// Database path + #[arg(long, env = "FM_PROOF_CACHE_DB")] + db_path: PathBuf, + }, + /// Get specific proof by instance ID + Get { + /// Database path + #[arg(long, env = "FM_PROOF_CACHE_DB")] + db_path: PathBuf, + + /// Instance ID to fetch + #[arg(long)] + instance_id: u64, + }, + /// Clear the cache + Clear { + /// Database path + #[arg(long, env = "FM_PROOF_CACHE_DB")] + db_path: PathBuf, + }, +} diff --git a/fendermint/app/src/cmd/mod.rs b/fendermint/app/src/cmd/mod.rs index 0338b18806..189109eb89 100644 --- a/fendermint/app/src/cmd/mod.rs +++ b/fendermint/app/src/cmd/mod.rs @@ -23,6 +23,7 @@ pub mod eth; pub mod genesis; pub mod key; pub mod materializer; +pub mod proof_cache; pub mod rpc; pub mod run; @@ -69,6 +70,7 @@ macro_rules! cmd { /// Execute the command specified in the options. pub async fn exec(opts: Arc) -> anyhow::Result<()> { match &opts.command { + Commands::ProofCache(args) => args.exec(()).await, Commands::Config(args) => args.exec(opts.clone()).await, Commands::Debug(args) => { let _trace_file_guard = set_global_tracing_subscriber(&TracingSettings::default()); diff --git a/fendermint/app/src/cmd/proof_cache.rs b/fendermint/app/src/cmd/proof_cache.rs new file mode 100644 index 0000000000..064e4d86d6 --- /dev/null +++ b/fendermint/app/src/cmd/proof_cache.rs @@ -0,0 +1,194 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::cmd; +use crate::options::proof_cache::{ProofCacheArgs, ProofCacheCommands}; +use fendermint_vm_topdown_proof_service::persistence::ProofCachePersistence; +use std::path::Path; + +cmd! { + ProofCacheArgs(self) { + handle_proof_cache_command(self) + } +} + +fn handle_proof_cache_command(args: &ProofCacheArgs) -> anyhow::Result<()> { + match &args.command { + ProofCacheCommands::Inspect { db_path } => inspect_cache(db_path), + ProofCacheCommands::Stats { db_path } => show_stats(db_path), + ProofCacheCommands::Get { + db_path, + instance_id, + } => get_proof(db_path, *instance_id), + ProofCacheCommands::Clear { db_path } => clear_cache(db_path), + } +} + +fn inspect_cache(db_path: &Path) -> anyhow::Result<()> { + println!("=== Proof Cache Inspection ==="); + println!("Database: {}", db_path.display()); + println!(); + + let persistence = ProofCachePersistence::open(db_path)?; + let entries = persistence.load_all_entries()?; + + if entries.is_empty() { + println!("\nCache is empty."); + return Ok(()); + } + + println!("\nEntries:"); + println!( + "{:<12} {:<20} {:<15} {:<15}", + "Instance ID", "Epochs", "Proof Size", "Signers" + ); + println!("{}", "-".repeat(70)); + + for entry in &entries { + let proof_size = fvm_ipld_encoding::to_vec(&entry.proof_bundle) + .map(|v| v.len()) + .unwrap_or(0); + + println!( + "{:<12} {:<20?} {:<15} {:<15}", + entry.certificate.gpbft_instance, + entry.certificate.ec_chain.suffix(), + format!("{} bytes", proof_size), + format!("{} signers", entry.certificate.signers.len()) + ); + } + + Ok(()) +} + +fn show_stats(db_path: &Path) -> anyhow::Result<()> { + println!("=== Proof Cache Statistics ==="); + println!("Database: {}", db_path.display()); + println!(); + + let persistence = ProofCachePersistence::open(db_path)?; + let entries = persistence.load_all_entries()?; + + if entries.is_empty() { + println!("Cache is empty."); + return Ok(()); + } + + println!("Count: {}", entries.len()); + println!( + "Instances: {} - {}", + entries + .first() + .map(|e| e.certificate.gpbft_instance) + .unwrap_or(0), + entries + .last() + .map(|e| e.certificate.gpbft_instance) + .unwrap_or(0) + ); + println!(); + + // Proof size statistics + let total_proof_size: usize = entries + .iter() + .map(|e| { + fvm_ipld_encoding::to_vec(&e.proof_bundle) + .map(|v| v.len()) + .unwrap_or(0) + }) + .sum(); + let avg_proof_size = total_proof_size / entries.len(); + + println!("Proof Bundle Statistics:"); + println!( + " Total Size: {} bytes ({:.2} MB)", + total_proof_size, + total_proof_size as f64 / 1024.0 / 1024.0 + ); + println!( + " Average Size: {} bytes ({:.2} KB)", + avg_proof_size, + avg_proof_size as f64 / 1024.0 + ); + + Ok(()) +} + +fn get_proof(db_path: &Path, instance_id: u64) -> anyhow::Result<()> { + println!("=== Get Proof for Instance {} ===", instance_id); + println!("Database: {}", db_path.display()); + println!(); + + let persistence = ProofCachePersistence::open(db_path)?; + let entries = persistence.load_all_entries()?; + + if entries.is_empty() { + println!("Cache is empty."); + return Ok(()); + } + + let entry = entries + .iter() + .find(|e| e.certificate.gpbft_instance == instance_id); + + if let Some(entry) = entry { + println!("Found proof for instance {}", instance_id); + println!(); + + // Certificate Details + println!("F3 Certificate:"); + println!(" Instance ID: {}", entry.certificate.gpbft_instance); + println!( + " Finalized Epochs: {:?}", + &entry.certificate.ec_chain.suffix() + ); + println!( + " BLS Signature: {} bytes", + entry.certificate.signature.len() + ); + println!(" Signers: {} validators", entry.certificate.signers.len()); + println!(); + + // Proof Bundle Summary + let proof_bundle_size = fvm_ipld_encoding::to_vec(&entry.proof_bundle) + .map(|v| v.len()) + .unwrap_or(0); + println!("Proof Bundle:"); + println!( + " Total Size: {} bytes ({:.2} KB)", + proof_bundle_size, + proof_bundle_size as f64 / 1024.0 + ); + + if let Some(proof_bundle) = &entry.proof_bundle { + println!(" Storage Proofs: {}", proof_bundle.storage_proofs.len()); + println!(" Event Proofs: {}", proof_bundle.event_proofs.len()); + println!(" Witness Blocks: {}", proof_bundle.blocks.len()); + println!(); + } else { + println!(" No proof bundle found"); + } + + // Metadata + println!("Metadata:"); + println!(" Generated At: {:?}", entry.generated_at); + println!(" Source RPC: {}", entry.source_rpc); + } else { + println!("No proof found for instance {}", instance_id); + println!(); + println!("Available instances: {:?}", entries.len()); + } + + Ok(()) +} + +fn clear_cache(db_path: &Path) -> anyhow::Result<()> { + println!("=== Clear Cache ==="); + println!("Database: {}", db_path.display()); + println!(); + + let persistence = ProofCachePersistence::open(db_path)?; + persistence.clear_all_entries()?; + + Ok(()) +} diff --git a/fendermint/vm/topdown/proof-service/Cargo.toml b/fendermint/vm/topdown/proof-service/Cargo.toml new file mode 100644 index 0000000000..b5ba35a98d --- /dev/null +++ b/fendermint/vm/topdown/proof-service/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "fendermint_vm_topdown_proof_service" +description = "Proof generator service" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +tokio = { workspace = true, features = ["sync", "time", "macros", "fs"] } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +parking_lot = { workspace = true } +url = { workspace = true } +base64 = { workspace = true } +humantime-serde = { workspace = true } +cid = { workspace = true } +multihash = { workspace = true } +rocksdb = { version = "0.21", features = ["multi-threaded-cf"] } +futures = { workspace = true } +fvm_ipld_bitfield = "0.7.2" +keccak-hash = "0.11" +num-bigint = { workspace = true } + +# Fendermint +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" } + +# Metrics +prometheus = { workspace = true } + +# FVM +fvm_shared = { workspace = true } +fvm_ipld_encoding = { workspace = true } + +proofs = { git = "https://github.com/consensus-shipyard/ipc-filecoin-proofs", branch = "proofs" } + +# F3 certificate handling +filecoin-f3-certs = { git = "https://github.com/moshababo/rust-f3", branch = "cargo-git-compat" } +filecoin-f3-rpc = { git = "https://github.com/moshababo/rust-f3", branch = "cargo-git-compat" } +filecoin-f3-lightclient = { git = "https://github.com/moshababo/rust-f3", branch = "cargo-git-compat" } +filecoin-f3-gpbft = { git = "https://github.com/moshababo/rust-f3", branch = "cargo-git-compat" } + +# Development/testing binary dependencies +clap = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true } +chrono = { version = "0.4", optional = true } + +[features] +# Feature for building the development/testing binary +dev-tools = ["clap", "tracing-subscriber", "chrono"] + +[[bin]] +name = "proof-cache-test" +path = "src/bin/proof-cache-test.rs" +required-features = ["dev-tools"] + +[dev-dependencies] +tokio = { workspace = true, features = ["test-util", "rt-multi-thread"] } +tracing-subscriber = { workspace = true } +multihash-codetable = { version = "0.1.4", features = ["blake2b"] } +tempfile = "3.8" diff --git a/fendermint/vm/topdown/proof-service/README.md b/fendermint/vm/topdown/proof-service/README.md new file mode 100644 index 0000000000..79750d1d39 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/README.md @@ -0,0 +1,633 @@ +# F3 Proof Generator Service + +Pre-generates cryptographic proofs for F3 certificates from the parent chain, caching them for instant use by block proposers. + +## Overview + +This service provides production-ready proof generation for IPC subnets using F3 finality from the parent chain. It combines: + +- **F3 Light Client** for cryptographic validation (BLS signatures, quorum, chain continuity) +- **Proof Generation** using the `ipc-filecoin-proofs` library +- **High-Performance Caching** with RocksDB persistence +- **Prometheus Metrics** for production monitoring + +## Features + +### Full Cryptographic Validation + +- BLS signature verification using F3 light client +- Quorum checks (>2/3 power) +- Chain continuity validation (sequential instances) +- Power table verification and tracking + +### High Performance + +- Pre-generates proofs ahead of time (configurable lookahead) +- In-memory cache with optional RocksDB persistence +- Sequential processing ensures F3 state consistency +- ~15KB proof bundles with 15-20 witness blocks + +### Production Ready + +- Prometheus metrics for monitoring +- Structured logging with tracing +- Configuration validation on startup +- Graceful error handling with detailed context +- Supports recent F3 instances (RPC lookback limit: ~16.7 hours) + +## Proof Specs + +The service generates proofs for the following on-chain data: + +### Storage Proofs + +| Target | Contract | Slot Offset | Purpose | +| --------------------------------- | -------- | ----------- | --------------------------------------------- | +| `subnets[subnetKey].topDownNonce` | Gateway | 3 | Verify top-down message ordering | +| `nextConfigurationNumber` | Gateway | 20 | Track configuration changes for power updates | + +### Event Proofs + +| Event | Contract | Signature | Purpose | +| ----------------------- | ----------------- | ------------------------------------------------------------ | ---------------------------------------- | +| `NewTopDownMessage` | LibGateway | `NewTopDownMessage(address,IpcEnvelope,bytes32)` | Cross-net messages from parent to subnet | +| `NewPowerChangeRequest` | LibPowerChangeLog | `NewPowerChangeRequest(PowerOperation,address,bytes,uint64)` | Validator power changes | + +## Architecture + +``` +┌──────────────┐ +│ Parent Chain │ +│ F3 RPC │ +└──────┬───────┘ + │ Fetch certificates + ↓ +┌──────────────────────────────────┐ +│ F3 Light Client │ +│ - Fetch from F3 RPC │ +│ - BLS signature verification │ +│ - Quorum validation (>2/3 power) │ +│ - Chain continuity checks │ +│ - Returns (certificate, power_table) │ +└──────┬───────────────────────────┘ + │ Validated certificate + power table + ↓ +┌──────────────────────────────────┐ +│ Proof Assembler │ +│ - Build (parent, child) pairs │ +│ - Generate storage proofs │ +│ - Generate event proofs │ +│ - Build witness blocks │ +└──────┬───────────────────────────┘ + │ Proof bundles (one per epoch) + ↓ +┌──────────────────────────────────┐ +│ Proof Cache (Memory + RocksDB) │ +│ - Certificate store (by instance)│ +│ - Epoch proof store (by epoch) │ +│ - Lookahead window │ +│ - Retention policy │ +└──────┬───────────────────────────┘ + │ + ↓ Query by proposers +┌──────────────────────────────────┐ +│ Block Proposer │ +│ - Get proof for epoch │ +│ - Include in block │ +│ - Mark epoch as committed │ +└──────────────────────────────────┘ +``` + +### Proof Generation Flow + +Each F3 certificate contains tipsets `[base, suffix...]` where: + +- `base` = last finalized epoch from previous certificate (overlap point) +- `suffix` = new epochs being finalized + +For each certificate, we generate proofs for all (parent, child) tipset pairs: + +``` +Certificate: [E0, E1, E2, E3] +Proofs generated: + - E0 (using E1 as child) + - E1 (using E2 as child) + - E2 (using E3 as child) + - E3 has no child yet → proven when next certificate arrives +``` + +**Why (parent, child) pairs?** Filecoin stores `parentReceipts` (containing events and state changes from executing epoch E) in the child block at epoch E+1, not in the parent block. + +## Components + +### F3Client (`src/f3_client.rs`) + +Wraps the F3 light client to provide: + +- Certificate fetching from F3 RPC +- Full cryptographic validation (BLS, quorum, continuity) +- Sequential state management (prevents instance skipping) +- Power table tracking and updates + +**Key Methods:** + +- `new(rpc, network, instance, power_table)` - Production constructor with power table from F3CertManager +- `new_from_rpc(rpc, network, instance)` - Testing constructor that fetches power table from RPC +- `fetch_and_validate()` - Fetch and validate next certificate, returns `(FinalityCertificate, PowerEntries)` + +### ProofAssembler (`src/assembler.rs`) + +Generates cryptographic proofs using the `ipc-filecoin-proofs` library: + +- Fetches parent and child tipsets from Lotus RPC +- Generates storage proofs for Gateway contract state +- Generates event proofs for cross-net messages and power changes +- Creates minimal Merkle witness blocks for verification + +### ProofCache (`src/cache.rs`) + +Two-level cache avoiding certificate duplication: + +- **Certificate Store**: `instance_id -> CertificateEntry` (stores each cert once) +- **Epoch Proof Store**: `epoch -> EpochProofEntry` (references cert by instance) + +Features: + +- In-memory BTreeMap for O(log n) ordered access +- Optional RocksDB persistence for crash recovery +- Lookahead window (pre-generate N instances ahead) +- Retention policy (keep M epochs after commitment) +- Automatic cleanup of orphaned certificates +- Prometheus metrics for hits/misses and cache size + +### ProofGeneratorService (`src/service.rs`) + +Background service that: + +- Polls F3 RPC at configured intervals +- Validates certificates cryptographically (returns cert + power table) +- Generates proofs for all (parent, child) pairs in certificate +- Caches certificate and epoch proofs +- Handles errors gracefully with retries +- Emits Prometheus metrics + +## Usage + +### In Fendermint Application + +```rust +use fendermint_vm_topdown_proof_service::{launch_service, ProofServiceConfig, CacheConfig}; +use fendermint_vm_topdown_proof_service::config::GatewayId; +use filecoin_f3_gpbft::PowerEntries; +use std::time::Duration; + +// Get initial state from F3CertManager actor +let initial_instance = f3_cert_manager.last_committed_instance(); +let initial_epoch = f3_cert_manager.last_committed_epoch(); +let power_table = f3_cert_manager.power_table(); + +// Configuration +let config = ProofServiceConfig { + enabled: true, + parent_rpc_url: "http://api.calibration.node.glif.io/rpc/v1".to_string(), + gateway_id: GatewayId::ActorId(1001), // or GatewayId::EthAddress("0x...") + cache_config: CacheConfig { + lookahead_instances: 5, + retention_epochs: 100, + }, + polling_interval: Duration::from_secs(30), + ..Default::default() +}; + +// Launch service with optional persistence +let db_path = Some(PathBuf::from("/var/lib/fendermint/proof-cache")); +let (cache, handle) = launch_service( + config, + subnet_id, + initial_epoch, + initial_instance, + power_table, + db_path, +).await?.unwrap(); + +// Query cache in block proposer - get proof with certificate +if let Some(entry) = cache.get_epoch_proof_with_certificate(epoch) { + // entry.proof_bundle - the cryptographic proof + // entry.certificate - the F3 certificate + // entry.finalized_tipsets - tipsets from the certificate + propose_block_with_proof(entry); +} + +// After block execution, mark epoch as committed +cache.mark_committed(epoch, instance)?; +``` + +### CLI Tools + +Inspect cache contents: + +```bash +ipc-cli proof-cache inspect --db-path /var/lib/fendermint/proof-cache +``` + +Show cache statistics: + +```bash +ipc-cli proof-cache stats --db-path /var/lib/fendermint/proof-cache +``` + +Get specific proof: + +```bash +ipc-cli proof-cache get --db-path /var/lib/fendermint/proof-cache --instance-id 12345 +``` + +### Standalone Testing + +```bash +# Build the test binary +cargo build --package fendermint_vm_topdown_proof_service --features cli --bin proof-cache-test + +# Get current F3 instance +LATEST=$(curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"Filecoin.F3GetLatestCertificate","params":[],"id":1}' \ + http://api.calibration.node.glif.io/rpc/v1 | jq -r '.result.GPBFTInstance') + +# Start from recent instance (within RPC lookback limit of ~16.7 hours) +START=$((LATEST - 5)) + +# Run against Calibration testnet +./target/debug/proof-cache-test run \ + --rpc-url "http://api.calibration.node.glif.io/rpc/v1" \ + --initial-instance $START \ + --gateway-actor-id 176609 \ + --subnet-id "calib-subnet-1" \ + --poll-interval 10 \ + --lookahead 3 \ + --db-path /tmp/proof-cache-test.db +``` + +## Configuration + +All configuration options in `ProofServiceConfig`: + +| Field | Type | Required | Description | +| ---------------------- | -------------- | -------- | ------------------------------------------------ | +| `enabled` | bool | Yes | Enable/disable the service | +| `parent_rpc_url` | String | Yes | F3 RPC endpoint URL (HTTP or HTTPS) | +| `parent_subnet_id` | String | Yes | Parent subnet ID (e.g., "/r314159") | +| `f3_network_name` | String | Yes | F3 network name ("calibrationnet", "mainnet") | +| `gateway_actor_id` | Option | Yes | Gateway actor ID on parent chain | +| `subnet_id` | Option | Yes | Current subnet ID for event filtering | +| `lookahead_instances` | u64 | Yes | How many instances to pre-generate (must be > 0) | +| `retention_instances` | u64 | Yes | How many old instances to keep (must be > 0) | +| `polling_interval` | Duration | Yes | How often to check for new certificates | +| `max_cache_size_bytes` | usize | No | Maximum cache size (0 = unlimited) | +| `fallback_rpc_urls` | Vec | No | Backup RPC endpoints for failover | + +## Observability + +### Prometheus Metrics + +**F3 Certificate Operations:** + +- `f3_cert_fetch_total{status}` - Certificate fetch attempts (success/failure) +- `f3_cert_fetch_latency_secs{status}` - Fetch latency histogram +- `f3_cert_validation_total{status}` - Validation attempts (success/failure) +- `f3_cert_validation_latency_secs{status}` - Validation latency histogram +- `f3_current_instance` - Current F3 instance in light client state + +**Proof Generation:** + +- `proof_generation_total{status}` - Proof generation attempts +- `proof_generation_latency_secs{status}` - Generation latency histogram +- `proof_bundle_size_bytes{type}` - Proof bundle size distribution + +**Cache Operations:** + +- `proof_cache_size` - Number of proofs in cache +- `proof_cache_last_committed` - Last committed F3 instance +- `proof_cache_highest_cached` - Highest cached F3 instance +- `proof_cache_hit_total{result}` - Cache hits and misses + +### Structured Logging + +The service uses `tracing` for structured logging with appropriate levels: + +- `ERROR` - Validation failures, proof generation errors, RPC failures +- `WARN` - Configuration issues, deprecated features +- `INFO` - Certificate validated, proof generated, cache updates +- `DEBUG` - Detailed operation flow, state transitions + +Set log level with `RUST_LOG`: + +```bash +RUST_LOG=info,fendermint_vm_topdown_proof_service=debug fendermint run +``` + +## Data Flow + +### Certificate Validation Flow + +1. **Poll** - Service checks for new F3 instances (every polling_interval) +2. **Fetch** - Light client fetches certificate from F3 RPC +3. **Validate** - Full cryptographic validation: + - Verify BLS aggregated signature + - Check quorum (>2/3 of power signed) + - Verify chain continuity (sequential instances) + - Apply power table deltas +4. **Return** - Returns `(FinalityCertificate, PowerEntries)` tuple + +### Proof Generation Flow + +1. **Build Pairs** - Create (parent, child) tipset pairs using `windows(2)` +2. **For Each Pair** - Generate proofs using `ipc-filecoin-proofs` library: + - Storage proofs for `topDownNonce` and `nextConfigurationNumber` + - Event proofs for `NewTopDownMessage` and `NewPowerChangeRequest` + - Minimal Merkle witness blocks +3. **Cache Certificate** - Store certificate with power table (once per instance) +4. **Cache Proofs** - Store epoch proofs referencing the certificate + +### Cache Structure + +```rust +// Certificate stored once per F3 instance +pub struct CertificateEntry { + pub certificate: FinalityCertificate, + pub power_table: PowerEntries, + pub source_rpc: String, + pub fetched_at: SystemTime, +} + +// Proof stored per epoch, references certificate by instance +pub struct EpochProofEntry { + pub epoch: ChainEpoch, + pub proof_bundle: UnifiedProofBundle, + pub cert_instance: u64, // Reference to certificate + pub generated_at: SystemTime, +} +``` + +## Troubleshooting + +### Common Issues + +**1. "lookbacks of more than 16h40m0s are disallowed"** + +The Lotus RPC endpoint won't serve tipsets older than ~16.7 hours. + +**Solution**: Start from a recent F3 instance: + +```bash +# Get latest instance +LATEST=$(curl -s -X POST ... | jq -r '.result.GPBFTInstance') +# Start from 5-10 instances back +START=$((LATEST - 5)) +``` + +**2. "expected instance X, found instance Y"** + +The F3 light client requires sequential validation. If proof generation fails, the state advances but the proof isn't cached, causing retry failures. + +**Solution**: The service automatically handles this by checking if F3 state is past an instance before retrying. + +**3. "Failed to fetch certificate from F3 RPC"** + +Network connectivity issue or invalid RPC endpoint. + +**Solution**: + +- Verify RPC endpoint is accessible +- Use HTTP instead of HTTPS if TLS issues occur +- Check `fallback_rpc_urls` configuration + +**4. macOS system-configuration panic** + +Older issue with reqwest library on macOS (now fixed in upstream). + +**Solution**: Already fixed in upstream `ipc-filecoin-proofs` (uses `.no_proxy()`) + +## Testing + +### Unit Tests + +```bash +cargo test --package fendermint_vm_topdown_proof_service --lib +``` + +**Test Coverage:** + +- F3 client creation and state management +- Cache operations (insert, get, cleanup) +- Persistence layer (RocksDB save/load) +- Configuration parsing +- 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 +``` + +## Dependencies + +### Core Dependencies + +- `filecoin-f3-lightclient` - F3 light client with BLS validation +- `filecoin-f3-certs` - F3 certificate types and validation +- `filecoin-f3-gpbft` - GPBFT consensus types (power tables) +- `proofs` - IPC proof generation library (`ipc-filecoin-proofs`) +- `rocksdb` - Optional persistence layer +- `ipc-observability` - Metrics and tracing + +### Repository Links + +- F3 Light Client: https://github.com/moshababo/rust-f3/tree/bdn_agg +- Proofs Library: https://github.com/consensus-shipyard/ipc-filecoin-proofs/tree/proofs + +## Module Documentation + +### `f3_client.rs` - F3 Certificate Handling + +Wraps `filecoin-f3-lightclient` to provide certificate fetching and validation. +Owned by `ProofGeneratorService` and accessed sequentially (no interior mutability needed). + +**Production Mode:** + +```rust +let client = F3Client::new(rpc_url, network, instance, power_table)?; +``` + +Uses power table from F3CertManager actor on-chain. + +**Testing Mode:** + +```rust +let client = F3Client::new_from_rpc(rpc_url, network, instance).await?; +``` + +Fetches power table from F3 RPC (for testing/development). + +**Fetching Certificates:** + +```rust +let (certificate, power_table) = client.fetch_and_validate().await?; +``` + +Returns the validated certificate and the updated power table. + +### `assembler.rs` - Proof Generation + +Uses `ipc-filecoin-proofs` to generate cryptographic proofs. + +**Storage Proofs:** + +- `subnets[subnetKey].topDownNonce` (slot offset 3) - message ordering +- `nextConfigurationNumber` (slot 20) - power change tracking + +**Event Proofs:** + +- `NewTopDownMessage(address,IpcEnvelope,bytes32)` - cross-net messages +- `NewPowerChangeRequest(PowerOperation,address,bytes,uint64)` - validator changes + +Creates `LotusClient` on-demand (not `Send`, so created per-request). + +### `cache.rs` - Proof Caching + +Two-level thread-safe cache using `Arc>`. + +**Structure:** + +- `certificates: BTreeMap` - by instance ID +- `epoch_proofs: BTreeMap` - by epoch + +**Features:** + +- Deduplication: certificate stored once, referenced by multiple epoch proofs +- Automatic cleanup: removes proofs below retention threshold, then orphaned certs +- Optional RocksDB persistence +- Prometheus metrics + +### `service.rs` - Background Service + +Main service loop that: + +1. Polls at configured interval +2. Fetches and validates next certificate +3. Builds (parent, child) tipset pairs from certificate +4. Generates proof for each pair +5. Caches certificate and all epoch proofs +6. Emits metrics on success/failure + +**Critical**: Processes F3 instances sequentially - never skips! + +### `observe.rs` - Observability + +Prometheus metrics and structured events using `ipc-observability`. + +**Metrics Registration:** + +```rust +use fendermint_vm_topdown_proof_service::observe::register_metrics; +register_metrics(&prometheus_registry)?; +``` + +### `persistence.rs` - RocksDB Storage + +Persistent storage for proof cache using RocksDB. + +**Schema:** + +- `meta:last_committed` - Last committed instance ID +- `meta:schema_version` - Database schema version +- `entry:{instance_id}` - Serialized cache entries + +### `verifier.rs` - Proof Verification + +Deterministic verification of proof bundles against F3 certificates. + +**Usage:** + +```rust +use fendermint_vm_topdown_proof_service::verify_proof_bundle; +verify_proof_bundle(&bundle, &certificate)?; +``` + +Validates storage proofs and event proofs using `ipc-filecoin-proofs` verifier. + +## Performance Characteristics + +### Typical Proof Bundle + +- **Size**: 15-17 KB +- **Storage Proofs**: 1 (for topDownNonce) +- **Event Proofs**: 0-N (depends on messages in that instance) +- **Witness Blocks**: 15-21 Merkle tree blocks +- **Generation Time**: ~1-2 seconds (network dependent) +- **Validation Time**: ~10ms (BLS signature check) + +### Cache Efficiency + +- **Memory**: ~20 KB per cached instance +- **Lookahead=5**: ~100 KB memory +- **RocksDB**: Similar disk usage + metadata overhead +- **Hit Rate**: >95% for sequential block production + +## Known Limitations + +1. **RPC Lookback Limit**: Can only generate proofs for instances within ~16.7 hours (Lotus RPC limitation) +2. **Sequential Processing**: Must validate instances in order (F3 light client requirement) +3. **Single RPC Endpoint**: Currently uses single endpoint (multi-provider support in future plan) +4. **No Batch Fetching**: Fetches certificates one at a time (could be optimized) + +## Future Improvements + +See Cursor plan "Custom RPC Client Integration" for: + +- Multi-provider failover using custom RPC client trait +- Health tracking and automatic recovery +- Integration with ParentClient for reliability + +## Related Code + +- **F3CertManager Actor**: `fendermint/actors/f3-cert-manager` - On-chain certificate storage +- **Gateway Contract**: `contracts/contracts/gateway` - Parent chain gateway +- **IPC Provider**: `ipc/provider` - Lotus RPC client +- **Fendermint App**: Integrates this service for topdown finality + +## License + +MIT OR Apache-2.0 - Protocol Labs diff --git a/fendermint/vm/topdown/proof-service/src/assembler.rs b/fendermint/vm/topdown/proof-service/src/assembler.rs new file mode 100644 index 0000000000..666128599b --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/assembler.rs @@ -0,0 +1,268 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Proof bundle assembler +//! +//! Generates cryptographic proofs for parent chain finality using the +//! ipc-filecoin-proofs library. The assembler is only responsible for +//! proof generation - it has no knowledge of cache entries or storage. + +use crate::observe::{OperationStatus, ProofBundleGenerated}; +use crate::types::FinalizedTipset; +use anyhow::{Context, Result}; +use fvm_ipld_encoding; +use ipc_observability::emit; +use proofs::{ + client::LotusClient, + proofs::{ + calculate_storage_slot, common::bundle::UnifiedProofBundle, generate_proof_bundle, + EventProofSpec, StorageProofSpec, + }, +}; +use std::time::Instant; +use url::Url; + +// Event signatures for proof generation +// These use Solidity's canonical format (type names, not ABI encoding) +// For contract bindings, see: contract_bindings::lib_gateway::NewTopDownMessageFilter +// and contract_bindings::lib_power_change_log::NewPowerChangeRequestFilter + +/// Event signature for NewTopDownMessage from LibGateway.sol +/// Event: NewTopDownMessage(address indexed subnet, IpcEnvelope message, bytes32 indexed id) +/// Bindings: contract_bindings::lib_gateway::NewTopDownMessageFilter +pub const NEW_TOPDOWN_MESSAGE_SIGNATURE: &str = "NewTopDownMessage(address,IpcEnvelope,bytes32)"; + +/// Event signature for NewPowerChangeRequest from LibPowerChangeLog.sol +/// Event: NewPowerChangeRequest(PowerOperation op, address validator, bytes payload, uint64 configurationNumber) +/// Bindings: contract_bindings::lib_power_change_log::NewPowerChangeRequestFilter +/// This captures validator power changes that need to be reflected in the subnet +pub const NEW_POWER_CHANGE_REQUEST_SIGNATURE: &str = + "NewPowerChangeRequest(PowerOperation,address,bytes,uint64)"; + +/// Storage slot offset for topDownNonce in the Subnet struct. +/// In the Gateway actor's subnets mapping: mapping(SubnetID => Subnet) +/// The Subnet struct field layout (see contracts/contracts/structs/Subnet.sol): +/// - id (SubnetID): slot 0-1 (SubnetID has 2 fields) +/// - stake (uint256): slot 2 +/// - topDownNonce (uint64): slot 3 +/// - appliedBottomUpNonce (uint64): slot 3 (packed with topDownNonce) +/// - genesisEpoch (uint256): slot 4 +/// +/// We need the nonce to verify top-down message ordering. +const TOPDOWN_NONCE_STORAGE_OFFSET: u64 = 3; + +/// Storage slot for nextConfigurationNumber in GatewayActorStorage +/// This is used to track configuration changes for power updates +/// Based on the storage layout, nextConfigurationNumber is at slot 20 +const NEXT_CONFIG_NUMBER_STORAGE_SLOT: u64 = 20; + +/// Assembles proof bundles from F3 certificates and parent chain data +/// +/// # Thread Safety +/// +/// LotusClient from the proofs library uses Rc/RefCell internally, so it's not Send. +/// We store the URL and create clients on-demand instead of storing the client. +pub struct ProofAssembler { + rpc_url: Url, + gateway_actor_id: u64, + subnet_id: String, +} + +impl ProofAssembler { + /// Create a new proof assembler + pub fn new(rpc_url: String, gateway_actor_id: u64, subnet_id: String) -> Result { + let url = Url::parse(&rpc_url).context("Failed to parse RPC URL")?; + Ok(Self { + rpc_url: url, + gateway_actor_id, + subnet_id, + }) + } + + fn build_storage_specs(&self) -> Vec { + vec![ + StorageProofSpec { + actor_id: self.gateway_actor_id, + slot: calculate_storage_slot(&self.subnet_id, TOPDOWN_NONCE_STORAGE_OFFSET), + }, + StorageProofSpec { + actor_id: self.gateway_actor_id, + slot: calculate_storage_slot("", NEXT_CONFIG_NUMBER_STORAGE_SLOT), + }, + ] + } + + fn build_event_specs(&self) -> Vec { + vec![ + EventProofSpec { + event_signature: NEW_TOPDOWN_MESSAGE_SIGNATURE.to_string(), + topic_1: self.subnet_id.clone(), + actor_id_filter: Some(self.gateway_actor_id), + }, + EventProofSpec { + event_signature: NEW_POWER_CHANGE_REQUEST_SIGNATURE.to_string(), + topic_1: String::new(), + actor_id_filter: Some(self.gateway_actor_id), + }, + ] + } + + /// Create a LotusClient for making requests + /// + /// LotusClient is not Send, so we create it on-demand in each async function + /// rather than storing it as a field. + fn create_client(&self) -> LotusClient { + LotusClient::new(self.rpc_url.clone(), None) + } + + /// Fetch a tipset by epoch from Lotus RPC + async fn fetch_tipset(&self, epoch: i64) -> Result { + let client = self.create_client(); + let json = client + .request( + "Filecoin.ChainGetTipSetByHeight", + serde_json::json!([epoch, null]), + ) + .await + .with_context(|| format!("Failed to fetch tipset at epoch {}", epoch))?; + + serde_json::from_value(json) + .with_context(|| format!("Failed to deserialize tipset at epoch {}", epoch)) + } + + /// Generate a proof bundle for a single epoch transition. + /// + /// This is the primary method for proof generation. It creates proofs for + /// the state and events at `parent_tipset`, using `child_tipset` to access + /// the resulting state root and receipts. + /// + /// # Arguments + /// * `parent_tipset` - The epoch to prove (state/events at this height) + /// * `child_tipset` - The epoch containing the resulting state root (typically parent_epoch + 1) + /// + /// # Returns + /// A UnifiedProofBundle containing storage proofs, event proofs, and witness blocks. + pub async fn generate_proof_for_epoch( + &self, + parent_tipset: FinalizedTipset, + child_tipset: FinalizedTipset, + ) -> Result { + let parent_epoch = parent_tipset.epoch; + let child_epoch = child_tipset.epoch; + + let generation_start = Instant::now(); + + tracing::debug!( + parent_epoch, + child_epoch, + "Generating proof for epoch - fetching tipsets" + ); + + // Fetch tipsets from Lotus and verify they match the expected ones + let parent_api = self.fetch_tipset(parent_epoch).await?; + let child_api = self.fetch_tipset(child_epoch).await?; + + parent_tipset + .verify_matches(&FinalizedTipset::try_from(&parent_api)?) + .context("Parent tipset mismatch")?; + child_tipset + .verify_matches(&FinalizedTipset::try_from(&child_api)?) + .context("Child tipset mismatch")?; + + // Generate the proof bundle + let bundle = self + .generate_proof_bundle_internal(parent_epoch, &parent_api, &child_api) + .await?; + + // Emit metrics + let bundle_size_bytes = fvm_ipld_encoding::to_vec(&bundle) + .map(|v| v.len()) + .unwrap_or(0); + + let latency = generation_start.elapsed().as_secs_f64(); + + emit(ProofBundleGenerated { + highest_epoch: parent_epoch, + storage_proofs: bundle.storage_proofs.len(), + event_proofs: bundle.event_proofs.len(), + witness_blocks: bundle.blocks.len(), + bundle_size_bytes, + status: OperationStatus::Success, + latency, + }); + + tracing::info!( + parent_epoch, + child_epoch, + storage_proofs = bundle.storage_proofs.len(), + event_proofs = bundle.event_proofs.len(), + witness_blocks = bundle.blocks.len(), + "Generated proof bundle for epoch" + ); + + Ok(bundle) + } + + /// Internal method to generate proof bundle from already-fetched tipsets + async fn generate_proof_bundle_internal( + &self, + epoch: i64, + parent_api: &proofs::client::types::ApiTipset, + child_api: &proofs::client::types::ApiTipset, + ) -> Result { + // Build specs fresh each time (external types don't implement Clone) + let storage_specs = self.build_storage_specs(); + let event_specs = self.build_event_specs(); + + tracing::debug!( + epoch, + storage_specs = storage_specs.len(), + event_specs = event_specs.len(), + "Generating proof bundle" + ); + + // Clone data for the blocking task + let parent_api = parent_api.clone(); + let child_api = child_api.clone(); + let lotus_client = self.create_client(); + + // Generate proof bundle in blocking task + // CRITICAL: The proofs library uses Rc/RefCell internally making LotusClient and + // related types non-Send. We must use spawn_blocking to run the proof generation + // in a separate thread. + tokio::task::spawn_blocking(move || { + tokio::runtime::Handle::current() + .block_on(generate_proof_bundle( + &lotus_client, + &parent_api, + &child_api, + storage_specs, + event_specs, + )) + .context("Failed to generate proof bundle") + }) + .await + .context("Failed to join proof generation task")? + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_assembler_creation() { + let assembler = ProofAssembler::new( + "http://localhost:1234".to_string(), + 1001, + "test-subnet".to_string(), + ); + assert!(assembler.is_ok()); + } + + #[test] + fn test_invalid_url() { + let assembler = + ProofAssembler::new("not a url".to_string(), 1001, "test-subnet".to_string()); + assert!(assembler.is_err()); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/bin/proof-cache-test.rs b/fendermint/vm/topdown/proof-service/src/bin/proof-cache-test.rs new file mode 100644 index 0000000000..a8f7f7e771 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/bin/proof-cache-test.rs @@ -0,0 +1,226 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Development/testing binary for the proof cache service +//! +//! NOTE: For production use, use `fendermint proof-cache` commands instead. +//! This binary is for development and CI testing only. + +use clap::{Parser, Subcommand}; +use fendermint_vm_topdown_proof_service::config::{CacheConfig, GatewayId, ProofServiceConfig}; +use fendermint_vm_topdown_proof_service::launch_service; +use fvm_ipld_encoding; +use ipc_api::subnet_id::SubnetID; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +#[derive(Parser)] +#[command(author, version, about = "Proof cache service - DEVELOPMENT TOOL")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Run the proof generation service (development/testing) + Run { + /// Parent RPC URL + #[arg(long)] + rpc_url: String, + + /// Subnet ID + #[arg(long)] + subnet_id: String, + + /// Gateway address (Ethereum address like 0xE4c61299c16323C4B58376b60A77F68Aa59afC8b) + #[arg(long)] + gateway_address: String, + + /// Lookahead window + #[arg(long, default_value = "3")] + lookahead: u64, + + /// Initial F3 instance to start from + #[arg(long)] + initial_instance: u64, + + /// Poll interval in seconds + #[arg(long, default_value = "10")] + poll_interval: u64, + + /// Optional database path for persistence + #[arg(long)] + db_path: Option, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Initialize tracing + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("fendermint_vm_topdown_proof_service=debug".parse()?), + ) + .init(); + + let cli = Cli::parse(); + + match cli.command { + Commands::Run { + rpc_url, + subnet_id, + gateway_address, + lookahead, + initial_instance, + poll_interval, + db_path, + } => { + run_service( + rpc_url, + subnet_id, + gateway_address, + lookahead, + initial_instance, + poll_interval, + db_path, + ) + .await + } + } +} + +async fn run_service( + rpc_url: String, + subnet_id: String, + gateway_address: String, + lookahead: u64, + initial_instance: u64, + poll_interval: u64, + db_path: Option, +) -> anyhow::Result<()> { + println!("=== Proof Cache Service (DEVELOPMENT) ==="); + println!("Configuration:"); + println!(" RPC URL: {}", rpc_url); + println!(" Subnet ID: {}", subnet_id); + println!(" Gateway Address: {}", gateway_address); + println!(" Lookahead: {} instances", lookahead); + println!(" Initial Instance: {}", initial_instance); + println!(" Poll Interval: {} seconds", poll_interval); + if let Some(path) = &db_path { + println!(" Database: {}", path.display()); + } else { + println!(" Database: In-memory only"); + } + println!(); + + println!("Starting proof cache service..."); + println!(); + println!( + "Fetching initial power table from F3 RPC (instance {})...", + initial_instance + ); + + let temp_client = fendermint_vm_topdown_proof_service::f3_client::F3Client::new_from_rpc( + &rpc_url, + "calibrationnet", + initial_instance, + ) + .await?; + + // Get the power table + let current_state = temp_client.get_state(); + let power_table = current_state.power_table.clone(); + + println!("Power table fetched: {} entries", power_table.0.len()); + println!( + "F3 state initialized at instance {} (ready to validate {} onwards)", + initial_instance, initial_instance + ); + + let subnet_id_parsed = SubnetID::from_str(&subnet_id)?; + + let config = ProofServiceConfig { + enabled: true, + polling_interval: Duration::from_secs(poll_interval), + cache_config: CacheConfig { + lookahead_instances: lookahead, + retention_epochs: 2, + }, + parent_rpc_url: rpc_url, + gateway_id: GatewayId::EthAddress(gateway_address), + }; + + let (cache, _handle) = launch_service( + config, + subnet_id_parsed, + initial_instance, + power_table, + db_path, + ) + .await? + .expect("Service should be enabled"); + println!("Service started successfully!"); + println!("Monitoring parent chain for F3 certificates..."); + println!(); + + // Monitor cache status + let mut last_size = 0; + loop { + tokio::time::sleep(Duration::from_secs(5)).await; + + let size = cache.len(); + let highest = cache.highest_cached_instance(); + let instances = cache.cached_instances(); + + print!("\x1B[2J\x1B[1;1H"); // Clear screen + println!("=== Proof Cache Status ==="); + println!( + "Timestamp: {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S") + ); + println!(); + println!("Cache Statistics:"); + println!(" Entries in cache: {}", size); + println!( + " Highest cached instance: {}", + highest.map_or("None".to_string(), |h| h.to_string()) + ); + println!(); + + if size > last_size { + println!("✓ New proofs generated: {}", size - last_size); + last_size = size; + } + + if let Some(&latest_instance) = instances.last() { + if let Some(entry) = cache.get(latest_instance) { + println!("Latest Cached Proof:"); + println!(" Instance ID: {}", entry.certificate.gpbft_instance); + println!(" EC Chain tipsets: {}", entry.certificate.ec_chain.len()); + let proof_size = fvm_ipld_encoding::to_vec(&entry.proof_bundle) + .map(|v| v.len()) + .unwrap_or(0); + println!(" Proof bundle size: {} bytes", proof_size); + println!(" Generated at: {:?}", entry.generated_at); + println!(); + } + } else { + println!("No proofs cached yet..."); + println!(); + } + + if size > 0 { + println!("Cached Instances:"); + print!(" "); + for instance in instances { + print!("{} ", instance); + } + println!(); + } + + println!(); + println!("Press Ctrl+C to stop..."); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/cache.rs b/fendermint/vm/topdown/proof-service/src/cache.rs new file mode 100644 index 0000000000..f088baf294 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/cache.rs @@ -0,0 +1,586 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Two-level cache for proof bundles with optional disk persistence +//! +//! # Architecture +//! +//! The cache is organized in two levels: +//! - **Certificate Store**: Stores F3 certificates keyed by instance ID +//! - **Epoch Proof Store**: Stores proof bundles keyed by epoch +//! +//! This design avoids duplicating certificates when multiple epochs +//! reference the same certificate + +use crate::config::CacheConfig; +use crate::observe::{ProofCached, CACHE_HIT_TOTAL, CACHE_SIZE}; +use crate::persistence::ProofCachePersistence; +use crate::types::{CertificateEntry, EpochProofEntry, EpochProofWithCertificate}; +use anyhow::{Context, Result}; +use fvm_shared::clock::ChainEpoch; +use ipc_observability::emit; +use parking_lot::RwLock; +use std::collections::BTreeMap; +use std::path::Path; +use std::sync::atomic::{AtomicI64, AtomicU64, Ordering}; +use std::sync::Arc; + +/// Thread-safe two-level cache for proof bundles +#[derive(Clone)] +pub struct ProofCache { + /// Certificate store: instance_id -> CertificateEntry + certificates: Arc>>, + + /// Epoch proof store: epoch -> EpochProofEntry + epoch_proofs: Arc>>, + + /// Configuration + config: CacheConfig, + + /// Last committed epoch (updated when proofs are used on-chain) + last_committed_epoch: Arc, + + /// Last committed F3 instance (updated when proofs are used on-chain) + last_committed_instance: Arc, + + /// Optional disk persistence + persistence: Option>, +} + +impl ProofCache { + /// Create a new proof cache (in-memory only) + pub fn new( + last_committed_epoch: ChainEpoch, + last_committed_instance: u64, + config: CacheConfig, + ) -> Self { + Self { + certificates: Arc::new(RwLock::new(BTreeMap::new())), + epoch_proofs: Arc::new(RwLock::new(BTreeMap::new())), + last_committed_epoch: Arc::new(AtomicI64::new(last_committed_epoch)), + last_committed_instance: Arc::new(AtomicU64::new(last_committed_instance)), + config, + persistence: None, + } + } + + /// Create a new proof cache with disk persistence + /// + /// Loads existing entries from disk on startup. If committed state exists in + /// persistence, uses the higher of persisted vs provided values. + pub fn new_with_persistence( + initial_committed_epoch: ChainEpoch, + initial_committed_instance: u64, + config: CacheConfig, + db_path: &Path, + ) -> Result { + let persistence = ProofCachePersistence::open(db_path)?; + + // Load certificates + let cert_entries = persistence + .load_all_certificates() + .context("Failed to load certificates from disk")?; + let certificates: BTreeMap = cert_entries + .into_iter() + .map(|e| (e.instance_id(), e)) + .collect(); + + // Load epoch proofs + let proof_entries = persistence + .load_all_epoch_proofs() + .context("Failed to load epoch proofs from disk")?; + let epoch_proofs: BTreeMap = + proof_entries.into_iter().map(|e| (e.epoch, e)).collect(); + + tracing::info!( + certificates = certificates.len(), + epoch_proofs = epoch_proofs.len(), + "Loaded cache from disk" + ); + + let cache = Self { + certificates: Arc::new(RwLock::new(certificates)), + epoch_proofs: Arc::new(RwLock::new(epoch_proofs)), + last_committed_epoch: Arc::new(AtomicI64::new(initial_committed_epoch)), + last_committed_instance: Arc::new(AtomicU64::new(initial_committed_instance)), + config, + persistence: Some(Arc::new(persistence)), + }; + + // Cleanup old entries + cache.cleanup_old_epochs(initial_committed_epoch)?; + + Ok(cache) + } + + /// Insert a certificate into the store + pub fn insert_certificate(&self, entry: CertificateEntry) -> Result<()> { + let instance_id = entry.instance_id(); + self.certificates.write().insert(instance_id, entry.clone()); + self.with_persistence(|p| p.save_certificate(&entry))?; + tracing::debug!(instance_id, "Inserted certificate into cache"); + Ok(()) + } + + /// Get a certificate by instance ID + pub fn get_certificate(&self, instance_id: u64) -> Option { + self.certificates.read().get(&instance_id).cloned() + } + + /// Check if a certificate exists + pub fn contains_certificate(&self, instance_id: u64) -> bool { + self.certificates.read().contains_key(&instance_id) + } + + /// Get the highest cached certificate instance ID + pub fn highest_cached_instance(&self) -> Option { + self.certificates.read().keys().max().copied() + } + + /// Insert epoch proofs into the cache + /// + /// This is typically called after processing a certificate, inserting + /// proofs for all epochs in the certificate's suffix. + pub fn insert_epoch_proofs(&self, entries: Vec) -> Result<()> { + if entries.is_empty() { + return Ok(()); + } + + let epochs: Vec = entries.iter().map(|e| e.epoch).collect(); + + { + let mut proofs = self.epoch_proofs.write(); + for entry in entries.iter() { + proofs.insert(entry.epoch, entry.clone()); + } + } + + self.with_persistence(|p| { + for entry in &entries { + p.save_epoch_proof(entry)?; + } + Ok(()) + })?; + + self.emit_cache_metrics(&epochs); + tracing::debug!(?epochs, "Inserted epoch proofs into cache"); + Ok(()) + } + + fn emit_cache_metrics(&self, epochs: &[ChainEpoch]) { + let cache_size = self.epoch_proofs.read().len(); + if let Some(highest) = self.highest_cached_epoch() { + for epoch in epochs { + emit(ProofCached { + instance: *epoch as u64, + cache_size, + highest_cached: highest as u64, + }); + } + } + CACHE_SIZE.set(cache_size as i64); + } + + /// Get proof for a specific epoch + /// + /// Returns the proof entry without the certificate. + /// Use `get_epoch_proof_with_certificate` for full data. + pub fn get_epoch_proof(&self, epoch: ChainEpoch) -> Option { + let result = self.epoch_proofs.read().get(&epoch).cloned(); + + // Record cache hit/miss + CACHE_HIT_TOTAL + .with_label_values(&[if result.is_some() { "hit" } else { "miss" }]) + .inc(); + + result + } + + /// Get proof for a specific epoch with its certificate + /// + /// This is the main query method for consumers. Returns everything + /// needed for verification, including the finalized tipsets. + pub fn get_epoch_proof_with_certificate( + &self, + epoch: ChainEpoch, + ) -> Option { + let proof_entry = self.get_epoch_proof(epoch)?; + let cert = self.get_certificate(proof_entry.cert_instance)?; + + Some(EpochProofWithCertificate::new(&proof_entry, &cert)) + } + + /// Check if an epoch proof exists + pub fn contains_epoch_proof(&self, epoch: ChainEpoch) -> bool { + self.epoch_proofs.read().contains_key(&epoch) + } + + /// Get the highest cached epoch + pub fn highest_cached_epoch(&self) -> Option { + self.epoch_proofs.read().keys().max().copied() + } + + /// Get the lowest cached epoch + pub fn lowest_cached_epoch(&self) -> Option { + self.epoch_proofs.read().keys().min().copied() + } + + /// Mark an epoch and instance as committed and trigger cleanup + pub fn mark_committed(&self, epoch: ChainEpoch, instance: u64) -> Result<()> { + let old_epoch = self.last_committed_epoch.swap(epoch, Ordering::Release); + let old_instance = self + .last_committed_instance + .swap(instance, Ordering::Release); + + tracing::info!( + old_epoch, + new_epoch = epoch, + old_instance, + new_instance = instance, + "Updated last committed epoch and instance" + ); + + self.cleanup_old_epochs(epoch) + } + + /// Get the last committed epoch and instance + pub fn last_committed(&self) -> (ChainEpoch, u64) { + ( + self.last_committed_epoch.load(Ordering::Acquire), + self.last_committed_instance.load(Ordering::Acquire), + ) + } + + /// Get the current last committed epoch + pub fn last_committed_epoch(&self) -> ChainEpoch { + self.last_committed_epoch.load(Ordering::Acquire) + } + + /// Get the current last committed F3 instance + pub fn last_committed_instance(&self) -> u64 { + self.last_committed_instance.load(Ordering::Acquire) + } + + /// Get the number of cached epoch proofs + pub fn epoch_proof_count(&self) -> usize { + self.epoch_proofs.read().len() + } + + /// Get the number of cached certificates + pub fn certificate_count(&self) -> usize { + self.certificates.read().len() + } + + /// Check if cache is empty + pub fn is_empty(&self) -> bool { + self.epoch_proofs.read().is_empty() + } + + /// Remove epochs older than the retention window + fn cleanup_old_epochs(&self, current_epoch: ChainEpoch) -> Result<()> { + let cutoff = current_epoch.saturating_sub(self.config.retention_epochs as i64); + + let epochs_to_remove = self.collect_epochs_before(cutoff); + if epochs_to_remove.is_empty() { + tracing::debug!(cutoff, "No old epochs to cleanup"); + return Ok(()); + } + + // Remove proofs first, then cleanup orphaned certificates + self.remove_epoch_proofs(&epochs_to_remove); + let certs_to_remove = self.collect_unreferenced_certs(); + self.remove_certificates(&certs_to_remove); + + self.persist_deletions(&epochs_to_remove, &certs_to_remove)?; + + CACHE_SIZE.set(self.epoch_proofs.read().len() as i64); + + tracing::debug!( + epochs_removed = epochs_to_remove.len(), + certs_removed = certs_to_remove.len(), + cutoff, + "Cleaned up old cache entries" + ); + + Ok(()) + } + + fn collect_epochs_before(&self, cutoff: ChainEpoch) -> Vec { + self.epoch_proofs + .read() + .keys() + .filter(|&&epoch| epoch < cutoff) + .copied() + .collect() + } + + /// Find certificates not referenced by any remaining proofs + fn collect_unreferenced_certs(&self) -> Vec { + let referenced: std::collections::HashSet = self + .epoch_proofs + .read() + .values() + .map(|p| p.cert_instance) + .collect(); + + self.certificates + .read() + .keys() + .filter(|id| !referenced.contains(id)) + .copied() + .collect() + } + + fn remove_epoch_proofs(&self, epochs: &[ChainEpoch]) { + let mut proofs = self.epoch_proofs.write(); + for epoch in epochs { + proofs.remove(epoch); + } + } + + fn remove_certificates(&self, cert_ids: &[u64]) { + let mut certs = self.certificates.write(); + for id in cert_ids { + certs.remove(id); + } + } + + fn persist_deletions(&self, epochs: &[ChainEpoch], cert_ids: &[u64]) -> Result<()> { + self.with_persistence(|p| { + for epoch in epochs { + p.delete_epoch_proof(*epoch)?; + } + for id in cert_ids { + p.delete_certificate(*id)?; + } + Ok(()) + }) + } + + /// Execute a function with persistence if enabled, otherwise no-op. + fn with_persistence(&self, f: F) -> Result<()> + where + F: FnOnce(&ProofCachePersistence) -> Result<()>, + { + if let Some(persistence) = &self.persistence { + f(persistence)?; + } + Ok(()) + } + + /// Get all cached epochs (for debugging) + pub fn cached_epochs(&self) -> Vec { + self.epoch_proofs.read().keys().copied().collect() + } + + /// Get all cached certificate instance IDs (for debugging) + pub fn cached_certificate_instances(&self) -> Vec { + self.certificates.read().keys().copied().collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + SerializableCertificateEntry, SerializableECChainEntry, SerializableF3Certificate, + SerializablePowerEntries, SerializablePowerEntry, SerializableSupplementalData, + }; + use proofs::proofs::common::bundle::UnifiedProofBundle; + use std::time::SystemTime; + + fn create_test_certificate(instance_id: u64, epochs: Vec) -> CertificateEntry { + use multihash_codetable::{Code, MultihashDigest}; + + let power_table_cid = cid::Cid::new_v1(0x55, Code::Blake2b256.digest(b"test")).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![1; 48], + }]), + source_rpc: "test".to_string(), + fetched_at: SystemTime::now(), + }; + + CertificateEntry::try_from(serializable).expect("valid certificate entry") + } + + fn create_test_epoch_proof(epoch: ChainEpoch, cert_instance: u64) -> EpochProofEntry { + EpochProofEntry::new( + epoch, + UnifiedProofBundle { + storage_proofs: vec![], + event_proofs: vec![], + blocks: vec![], + }, + cert_instance, + ) + } + + #[test] + fn test_cache_basic_operations() { + let config = CacheConfig { + lookahead_instances: 10, + retention_epochs: 5, + }; + + let cache = ProofCache::new(100, 0, config); + + assert!(cache.is_empty()); + assert_eq!(cache.epoch_proof_count(), 0); + assert_eq!(cache.certificate_count(), 0); + + // Insert certificates + let cert1 = create_test_certificate(5, vec![100, 101, 102]); + let cert2 = create_test_certificate(6, vec![102, 103]); + cache.insert_certificate(cert1).unwrap(); + cache.insert_certificate(cert2).unwrap(); + + assert_eq!(cache.certificate_count(), 2); + assert!(cache.contains_certificate(5)); + assert!(cache.contains_certificate(6)); + + // Insert epoch proofs + let proofs = vec![ + create_test_epoch_proof(100, 5), + create_test_epoch_proof(101, 5), + create_test_epoch_proof(102, 5), + ]; + cache.insert_epoch_proofs(proofs).unwrap(); + + assert_eq!(cache.epoch_proof_count(), 3); + assert!(cache.contains_epoch_proof(100)); + assert!(cache.contains_epoch_proof(101)); + assert!(cache.contains_epoch_proof(102)); + } + + #[test] + fn test_get_epoch_proof_with_certificate() { + let config = CacheConfig { + lookahead_instances: 10, + retention_epochs: 5, + }; + + let cache = ProofCache::new(100, 0, config); + + // Insert certificates + let cert1 = create_test_certificate(5, vec![100, 101, 102]); + let cert2 = create_test_certificate(6, vec![102, 103]); + cache.insert_certificate(cert1).unwrap(); + cache.insert_certificate(cert2).unwrap(); + + // Insert epoch proof + let proof = create_test_epoch_proof(101, 5); + cache.insert_epoch_proofs(vec![proof]).unwrap(); + + // Get with certificate + let result = cache.get_epoch_proof_with_certificate(101); + assert!(result.is_some()); + + let entry = result.unwrap(); + assert_eq!(entry.epoch, 101); + assert_eq!(entry.certificate.gpbft_instance, 5); + assert!(!entry.finalized_tipsets.is_empty()); + } + + #[test] + fn test_cache_cleanup() { + let config = CacheConfig { + lookahead_instances: 10, + retention_epochs: 2, + }; + + let cache = ProofCache::new(100, 0, config); + + // Insert certificates + let cert1 = create_test_certificate(5, vec![100, 101, 102]); + let cert2 = create_test_certificate(6, vec![102, 103, 104]); + let cert3 = create_test_certificate(7, vec![104, 105]); + cache.insert_certificate(cert1).unwrap(); + cache.insert_certificate(cert2).unwrap(); + cache.insert_certificate(cert3).unwrap(); + + // Insert epoch proofs + let proofs = vec![ + create_test_epoch_proof(100, 5), + create_test_epoch_proof(101, 5), + create_test_epoch_proof(102, 5), + create_test_epoch_proof(103, 6), + create_test_epoch_proof(104, 6), + ]; + cache.insert_epoch_proofs(proofs).unwrap(); + + assert_eq!(cache.epoch_proof_count(), 5); + + // Mark epoch 104, instance 7 as committed (retention is 2) + // Should remove epochs < 102 (i.e., 100, 101) + cache.mark_committed(104, 7).unwrap(); + + assert_eq!(cache.epoch_proof_count(), 3); // 102, 103, 104 remain + assert!(!cache.contains_epoch_proof(100)); + assert!(!cache.contains_epoch_proof(101)); + assert!(cache.contains_epoch_proof(102)); + assert!(cache.contains_epoch_proof(103)); + assert!(cache.contains_epoch_proof(104)); + + // Certificate 5 might be removed if no longer referenced + // (depends on which proofs still reference it) + } + + #[test] + fn test_highest_cached_epoch() { + let config = CacheConfig { + lookahead_instances: 10, + retention_epochs: 5, + }; + + let cache = ProofCache::new(100, 0, config); + + assert_eq!(cache.highest_cached_epoch(), None); + + // Insert certificates first + cache + .insert_certificate(create_test_certificate(5, vec![100])) + .unwrap(); + cache + .insert_certificate(create_test_certificate(6, vec![101])) + .unwrap(); + + cache + .insert_epoch_proofs(vec![create_test_epoch_proof(100, 5)]) + .unwrap(); + assert_eq!(cache.highest_cached_epoch(), Some(100)); + + cache + .insert_epoch_proofs(vec![create_test_epoch_proof(105, 5)]) + .unwrap(); + assert_eq!(cache.highest_cached_epoch(), Some(105)); + + cache + .insert_epoch_proofs(vec![create_test_epoch_proof(103, 5)]) + .unwrap(); + assert_eq!(cache.highest_cached_epoch(), Some(105)); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/config.rs b/fendermint/vm/topdown/proof-service/src/config.rs new file mode 100644 index 0000000000..c2820d2975 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/config.rs @@ -0,0 +1,132 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Configuration for the proof generator service + +use ipc_api::subnet_id::SubnetID; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +const FILECOIN_MAINNET_CHAIN_ID: u64 = 314; +const FILECOIN_CALIBRATION_CHAIN_ID: u64 = 314159; + +/// Represents a value that can be either a numeric Actor ID or an Ethereum address string. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GatewayId { + /// Actor ID (u64) + ActorId(u64), + /// Ethereum address (String) + EthAddress(String), +} + +/// Configuration for the proof generator service +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofServiceConfig { + /// Enable/disable the service + pub enabled: bool, + + /// Polling interval for checking parent chain + #[serde(with = "humantime_serde")] + pub polling_interval: Duration, + + /// Configuration for the proof cache + pub cache_config: CacheConfig, + + /// Lotus/parent RPC endpoint URL + pub parent_rpc_url: String, + + /// Gateway identification on parent chain. + /// Can be an Actor ID (u64) or an Ethereum address (String). + pub gateway_id: GatewayId, +} + +impl ProofServiceConfig { + /// Validate the configuration. + /// + /// Returns an error if any required fields are missing or invalid. + pub fn validate(&self) -> anyhow::Result<()> { + if !self.enabled { + return Ok(()); // No validation needed if disabled + } + + if self.parent_rpc_url.is_empty() { + anyhow::bail!("parent_rpc_url is required when service is enabled"); + } + + url::Url::parse(&self.parent_rpc_url).map_err(|e| { + anyhow::anyhow!("Invalid parent_rpc_url '{}': {}", self.parent_rpc_url, e) + })?; + + if self.cache_config.lookahead_instances == 0 { + anyhow::bail!("lookahead_instances must be > 0"); + } + + Ok(()) + } + + 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() + } + } + } +} + +impl Default for ProofServiceConfig { + fn default() -> Self { + Self { + enabled: false, + polling_interval: Duration::from_secs(10), + cache_config: Default::default(), + parent_rpc_url: String::new(), + gateway_id: GatewayId::ActorId(0), + } + } +} + +/// Configuration for the proof cache +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheConfig { + /// How many F3 instances ahead of last_committed_instance to stay. + /// The service will stop fetching new certificates once: + /// current_instance >= last_committed_instance + lookahead_instances + pub lookahead_instances: u64, + + /// How many epochs to retain after they've been committed. + /// Old epochs outside this window will be cleaned up. + pub retention_epochs: u64, +} + +impl Default for CacheConfig { + fn default() -> Self { + Self { + // Default: stay ~20 instances ahead + lookahead_instances: 20, + // Default: keep proofs for 10 epochs after commit + retention_epochs: 10, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_config() { + let config = ProofServiceConfig::default(); + assert!(!config.enabled); + assert_eq!(config.polling_interval, Duration::from_secs(10)); + assert_eq!(config.cache_config.lookahead_instances, 20); + assert_eq!(config.cache_config.retention_epochs, 10); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/f3_client.rs b/fendermint/vm/topdown/proof-service/src/f3_client.rs new file mode 100644 index 0000000000..40cc9da2f2 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/f3_client.rs @@ -0,0 +1,275 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! F3 client wrapper for certificate fetching and validation +//! +//! Wraps the F3 light client to provide: +//! - Certificate fetching from F3 RPC +//! - Full cryptographic validation (BLS signatures, quorum, chain continuity) +//! - Sequential state management for validated certificates + +use crate::observe::{F3CertificateFetched, F3CertificateValidated, OperationStatus}; +use anyhow::{Context, Result}; +use filecoin_f3_certs::FinalityCertificate; +use filecoin_f3_gpbft::PowerEntries; +use filecoin_f3_lightclient::{LightClient, LightClientState}; +use ipc_observability::emit; +use std::time::Instant; +use tracing::{debug, error, info}; + +/// F3 client for fetching and validating certificates +/// +/// Uses the F3 light client for: +/// - Direct F3 RPC access +/// - Full cryptographic validation (BLS signatures, quorum, continuity) +/// - Stateful sequential validation +/// +/// This client is designed to be owned by a single service and accessed +/// sequentially. Methods that mutate state take `&mut self`. +pub struct F3Client { + /// Light client for F3 RPC and cryptographic validation + light_client: LightClient, + + /// Current validated state (instance, chain, power table) + state: LightClientState, + + /// F3 RPC endpoint + rpc_endpoint: String, +} + +impl F3Client { + /// Create a new F3 client with provided power table (PRODUCTION USE) + /// + /// This is the primary constructor for production use. The power table and + /// initial instance should come from the F3CertManager actor on-chain. + /// + /// # Arguments + /// * `rpc_endpoint` - F3 RPC endpoint + /// * `network_name` - Network name (e.g., "calibrationnet", "mainnet") + /// * `initial_instance` - F3 instance to bootstrap from (from F3CertManager actor) + /// * `initial_power_table` - Initial trusted power table (from F3CertManager actor) + pub fn new( + rpc_endpoint: &str, + network_name: &str, + initial_instance: u64, + initial_power_table: filecoin_f3_gpbft::PowerEntries, + ) -> Result { + let light_client = LightClient::new(rpc_endpoint, network_name) + .context("Failed to create F3 light client")?; + + // Initialize state with provided power table from actor + let state = LightClientState { + instance: initial_instance, + chain: None, + power_table: initial_power_table.clone(), + }; + + info!( + initial_instance, + power_table_size = initial_power_table.len(), + network = network_name, + rpc = rpc_endpoint, + "Created F3 client with power table from F3CertManager actor" + ); + + Ok(Self { + light_client, + state, + rpc_endpoint: rpc_endpoint.to_string(), + }) + } + + /// Create F3 client by fetching power table from RPC (TESTING ONLY) + /// + /// For testing/development. In production, use `new()` with power table from + /// the F3CertManager actor on-chain. + /// + /// # Arguments + /// * `rpc_endpoint` - F3 RPC endpoint + /// * `network_name` - Network name (e.g., "calibrationnet", "mainnet") + /// * `initial_instance` - F3 instance to bootstrap from + #[doc(hidden)] + pub async fn new_from_rpc( + rpc_endpoint: &str, + network_name: &str, + initial_instance: u64, + ) -> Result { + let mut light_client = LightClient::new(rpc_endpoint, network_name) + .context("Failed to create F3 light client")?; + + // Fetch initial power table from RPC (for testing) + let state = light_client + .initialize(initial_instance) + .await + .context("Failed to initialize light client with power table from RPC")?; + + info!( + initial_instance, + power_table_size = state.power_table.len(), + network = network_name, + "Created F3 client with power table from RPC (testing mode)" + ); + + Ok(Self { + light_client, + state, + rpc_endpoint: rpc_endpoint.to_string(), + }) + } + + /// Fetch and validate an F3 certificate + /// + /// This performs full cryptographic validation including: + /// - BLS signature correctness + /// - Quorum requirements (>2/3 power) + /// - Chain continuity (sequential instances) + /// - Power table validity + /// + /// # Returns + /// `FinalityCertificate` that has been cryptographically verified + pub async fn fetch_and_validate(&mut self) -> Result<(FinalityCertificate, PowerEntries)> { + let instance = self.state.instance + 1; + + debug!(instance, "Starting F3 certificate fetch and validation"); + + // Fetch certificate from F3 RPC first + let certificate = self.fetch_certificate(instance).await?; + + // Then validate the certificate cryptography + debug!(instance, "Validating certificate cryptography"); + let new_state = self.validate_certificate(&certificate)?; + let power_table = new_state.power_table.clone(); + + debug!( + instance, + current_instance = self.state.instance, + power_table_entries = self.state.power_table.len(), + "Current F3 validator state" + ); + + // Update the state with the new validated state + self.state = new_state; + + debug!(instance, "Certificate validation complete"); + + Ok((certificate, power_table)) + } + + async fn fetch_certificate(&self, instance: u64) -> Result { + let fetch_start = Instant::now(); + + match self.light_client.get_certificate(instance).await { + Ok(cert) => { + let latency = fetch_start.elapsed().as_secs_f64(); + emit(F3CertificateFetched { + instance, + ec_chain_len: cert.ec_chain.suffix().len(), + status: OperationStatus::Success, + latency, + }); + debug!( + instance, + ec_chain_len = cert.ec_chain.suffix().len(), + "Fetched certificate from F3 RPC" + ); + Ok(cert) + } + Err(e) => { + let latency = fetch_start.elapsed().as_secs_f64(); + emit(F3CertificateFetched { + instance, + ec_chain_len: 0, + status: OperationStatus::Failure, + latency, + }); + error!( + instance, + error = %e, + "Failed to fetch certificate from F3 RPC" + ); + Err(e).context("Failed to fetch certificate from F3 RPC") + } + } + } + + fn validate_certificate( + &mut self, + certificate: &FinalityCertificate, + ) -> Result { + let validation_start = Instant::now(); + let instance = certificate.gpbft_instance; + + match self + .light_client + .validate_certificates(&self.state, &[certificate.clone()]) + { + Ok(new_state) => { + let latency = validation_start.elapsed().as_secs_f64(); + emit(F3CertificateValidated { + instance, + new_instance: new_state.instance, + power_table_size: new_state.power_table.len(), + status: OperationStatus::Success, + latency, + }); + info!( + instance, + new_instance = new_state.instance, + power_table_size = new_state.power_table.len(), + "Certificate validated (BLS signatures, quorum, continuity)" + ); + Ok(new_state) + } + Err(e) => { + let latency = validation_start.elapsed().as_secs_f64(); + emit(F3CertificateValidated { + instance, + new_instance: self.state.instance, + power_table_size: self.state.power_table.len(), + status: OperationStatus::Failure, + latency, + }); + error!( + instance, + error = %e, + current_instance = self.state.instance, + power_table_entries = self.state.power_table.len(), + "Certificate validation failed" + ); + Err(e).context("Certificate cryptographic validation failed") + } + } + } + + /// Get current instance + pub fn current_instance(&self) -> u64 { + self.state.instance + } + + /// Get current validated state + pub fn get_state(&self) -> &LightClientState { + &self.state + } + + /// Get F3 RPC endpoint + pub fn rpc_endpoint(&self) -> &str { + &self.rpc_endpoint + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_f3_client_creation() { + use filecoin_f3_gpbft::PowerEntries; + + // Creating a client requires actual RPC endpoint + // Real test would need integration test with live network + let power_table = PowerEntries(vec![]); + + let result = F3Client::new("http://localhost:1234", "calibrationnet", 0, power_table); + + assert!(result.is_ok()); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/lib.rs b/fendermint/vm/topdown/proof-service/src/lib.rs new file mode 100644 index 0000000000..cc8c51bb87 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/lib.rs @@ -0,0 +1,173 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Proof generator service for F3-based parent finality +//! +//! This crate implements a background service that: +//! - Monitors the parent chain for new F3 certificates +//! - Generates proof bundles ahead of time (one per epoch) +//! - Caches proofs keyed by epoch for instant use by block proposers +//! - Ensures sequential processing of F3 instances +//! +//! # Architecture +//! +//! The cache uses a two-level structure: +//! - **Certificate Store**: F3 certificates keyed by instance ID +//! - **Epoch Proof Store**: Proof bundles keyed by epoch +//! +//! This avoids duplicating certificates when multiple epochs reference +//! the same certificate. + +pub mod assembler; +pub mod cache; +pub mod config; +pub mod f3_client; +pub mod observe; +pub mod persistence; +pub mod service; +pub mod types; +pub mod verifier; + +// Re-export main types for convenience +pub use cache::ProofCache; +pub use config::{CacheConfig, ProofServiceConfig}; +pub use service::ProofGeneratorService; +pub use types::{ + CertificateEntry, EpochProofEntry, EpochProofWithCertificate, SerializableF3Certificate, +}; +pub use verifier::ProofVerifier; + +use anyhow::{Context, Result}; +use fvm_shared::clock::ChainEpoch; +use ipc_api::subnet_id::SubnetID; +use std::sync::Arc; + +/// Initialize and launch the proof generator service +/// +/// This is the main entry point for starting the service. +/// It creates the cache, initializes the service, and spawns the background task. +/// +/// # Arguments +/// * `config` - Service configuration +/// * `subnet_id` - The subnet ID +/// * `initial_committed_epoch` - The last committed epoch (for cache initialization) +/// * `initial_instance` - The last committed F3 instance (from F3CertManager actor) +/// * `initial_power_table` - Initial power table (from F3CertManager actor) +/// * `db_path` - Optional database path for persistence +/// +/// # Returns +/// * `Arc` - Shared cache that proposers can query +/// * `tokio::task::JoinHandle` - Handle to the background service task +pub async fn launch_service( + config: ProofServiceConfig, + subnet_id: SubnetID, + initial_committed_epoch: ChainEpoch, + initial_instance: u64, + initial_power_table: filecoin_f3_gpbft::PowerEntries, + db_path: Option, +) -> Result, tokio::task::JoinHandle<()>)>> { + // Check if disabled first + if !config.enabled { + tracing::info!("Proof service is disabled in configuration"); + return Ok(None); + } + + // Validate configuration + config + .validate() + .context("Invalid proof service configuration")?; + + tracing::info!( + initial_epoch = initial_committed_epoch, + initial_instance, + parent_rpc = config.parent_rpc_url, + f3_network = config.f3_network_name(&subnet_id), + lookahead_instances = config.cache_config.lookahead_instances, + "Launching proof generator service with validated configuration" + ); + + // Create cache (with optional persistence) + let cache = if let Some(path) = db_path { + tracing::info!(path = %path.display(), "Creating cache with persistence"); + Arc::new(ProofCache::new_with_persistence( + initial_committed_epoch, + initial_instance, + config.cache_config.clone(), + &path, + )?) + } else { + tracing::info!("Creating in-memory cache (no persistence)"); + Arc::new(ProofCache::new( + initial_committed_epoch, + initial_instance, + config.cache_config.clone(), + )) + }; + + // Clone what we need for the background task + let config_clone = config.clone(); + let cache_clone = cache.clone(); + let power_table_clone = initial_power_table.clone(); + + // Spawn background task + let handle = tokio::spawn(async move { + match ProofGeneratorService::new( + config_clone, + cache_clone, + &subnet_id, + initial_instance, + power_table_clone, + ) + .await + { + Ok(service) => service.run().await, + Err(e) => { + tracing::error!(error = %e, "Failed to create proof generator service"); + } + } + }); + + Ok(Some((cache, handle))) +} + +#[cfg(test)] +mod tests { + use super::*; + use filecoin_f3_gpbft::PowerEntries; + + #[tokio::test] + async fn test_launch_service_disabled() { + let config = ProofServiceConfig { + enabled: false, + ..Default::default() + }; + + let power_table = PowerEntries(vec![]); + let subnet_id = SubnetID::default(); + let result = launch_service(config, subnet_id, 0, 0, power_table, None).await; + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + + #[tokio::test] + async fn test_launch_service_enabled() { + use crate::config::GatewayId; + use filecoin_f3_gpbft::PowerEntries; + + let config = ProofServiceConfig { + enabled: true, + parent_rpc_url: "http://localhost:1234/rpc/v1".to_string(), + gateway_id: GatewayId::ActorId(1001), + polling_interval: std::time::Duration::from_secs(60), + ..Default::default() + }; + + let power_table = PowerEntries(vec![]); + let subnet_id = SubnetID::default(); + + let result = launch_service(config, subnet_id, 100, 5, power_table, None).await; + assert!(result.is_ok()); + + let (_cache, handle) = result.unwrap().unwrap(); + handle.abort(); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/observe.rs b/fendermint/vm/topdown/proof-service/src/observe.rs new file mode 100644 index 0000000000..101ff8b3cd --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/observe.rs @@ -0,0 +1,204 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Observability and metrics for the F3 proof service + +use ipc_observability::{ + impl_traceable, impl_traceables, lazy_static, register_metrics, Recordable, TraceLevel, + Traceable, +}; +use prometheus::{ + register_histogram_vec, register_int_counter_vec, register_int_gauge, HistogramVec, + IntCounterVec, IntGauge, Registry, +}; + +/// Operation status for metrics +#[derive(Debug, Clone, Copy)] +pub enum OperationStatus { + Success, + Failure, +} + +impl OperationStatus { + pub fn as_str(&self) -> &'static str { + match self { + OperationStatus::Success => "success", + OperationStatus::Failure => "failure", + } + } +} + +register_metrics! { + // F3 Certificate Operations + F3_CERT_FETCH_TOTAL: IntCounterVec + = register_int_counter_vec!("f3_cert_fetch_total", "F3 certificate fetch attempts", &["status"]); + F3_CERT_FETCH_LATENCY_SECS: HistogramVec + = register_histogram_vec!("f3_cert_fetch_latency_secs", "F3 certificate fetch latency", &["status"]); + F3_CERT_VALIDATION_TOTAL: IntCounterVec + = register_int_counter_vec!("f3_cert_validation_total", "F3 certificate validations", &["status"]); + F3_CERT_VALIDATION_LATENCY_SECS: HistogramVec + = register_histogram_vec!("f3_cert_validation_latency_secs", "F3 certificate validation latency", &["status"]); + F3_CURRENT_INSTANCE: IntGauge + = register_int_gauge!("f3_current_instance", "Current F3 instance in light client state"); + + // Proof Generation + PROOF_GENERATION_TOTAL: IntCounterVec + = register_int_counter_vec!("proof_generation_total", "Proof bundle generation attempts", &["status"]); + PROOF_GENERATION_LATENCY_SECS: HistogramVec + = register_histogram_vec!("proof_generation_latency_secs", "Proof bundle generation latency", &["status"]); + PROOF_BUNDLE_SIZE_BYTES: HistogramVec + = register_histogram_vec!("proof_bundle_size_bytes", "Proof bundle sizes", &["type"]); + + // Cache Operations + CACHE_SIZE: IntGauge + = register_int_gauge!("proof_cache_size", "Number of proofs in cache"); + CACHE_LAST_COMMITTED: IntGauge + = register_int_gauge!("proof_cache_last_committed", "Last committed F3 instance"); + CACHE_HIGHEST_CACHED: IntGauge + = register_int_gauge!("proof_cache_highest_cached", "Highest cached F3 instance"); + CACHE_HIT_TOTAL: IntCounterVec + = register_int_counter_vec!("proof_cache_hit_total", "Cache hits/misses", &["result"]); + CACHE_INSERT_TOTAL: IntCounterVec + = register_int_counter_vec!("proof_cache_insert_total", "Cache insertions", &["status"]); +} + +impl_traceables!( + TraceLevel::Info, + "F3ProofService", + F3CertificateFetched, + F3CertificateValidated, + ProofBundleGenerated, + ProofCached +); + +#[derive(Debug)] +pub struct F3CertificateFetched { + pub instance: u64, + pub ec_chain_len: usize, + pub status: OperationStatus, + pub latency: f64, +} + +impl Recordable for F3CertificateFetched { + fn record_metrics(&self) { + F3_CERT_FETCH_TOTAL + .with_label_values(&[self.status.as_str()]) + .inc(); + F3_CERT_FETCH_LATENCY_SECS + .with_label_values(&[self.status.as_str()]) + .observe(self.latency); + } +} + +#[derive(Debug)] +pub struct F3CertificateValidated { + pub instance: u64, + pub new_instance: u64, + pub power_table_size: usize, + pub status: OperationStatus, + pub latency: f64, +} + +impl Recordable for F3CertificateValidated { + fn record_metrics(&self) { + F3_CERT_VALIDATION_TOTAL + .with_label_values(&[self.status.as_str()]) + .inc(); + F3_CERT_VALIDATION_LATENCY_SECS + .with_label_values(&[self.status.as_str()]) + .observe(self.latency); + if matches!(self.status, OperationStatus::Success) { + F3_CURRENT_INSTANCE.set(self.new_instance as i64); + } + } +} + +#[derive(Debug)] +pub struct ProofBundleGenerated { + pub highest_epoch: i64, + pub storage_proofs: usize, + pub event_proofs: usize, + pub witness_blocks: usize, + pub bundle_size_bytes: usize, + pub status: OperationStatus, + pub latency: f64, +} + +impl Recordable for ProofBundleGenerated { + fn record_metrics(&self) { + PROOF_GENERATION_TOTAL + .with_label_values(&[self.status.as_str()]) + .inc(); + PROOF_GENERATION_LATENCY_SECS + .with_label_values(&[self.status.as_str()]) + .observe(self.latency); + if matches!(self.status, OperationStatus::Success) { + PROOF_BUNDLE_SIZE_BYTES + .with_label_values(&["total"]) + .observe(self.bundle_size_bytes as f64); + } + } +} + +#[derive(Debug)] +pub struct ProofCached { + pub instance: u64, + pub cache_size: usize, + pub highest_cached: u64, +} + +impl Recordable for ProofCached { + fn record_metrics(&self) { + CACHE_SIZE.set(self.cache_size as i64); + CACHE_HIGHEST_CACHED.set(self.highest_cached as i64); + CACHE_INSERT_TOTAL.with_label_values(&["success"]).inc(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ipc_observability::emit; + + #[test] + fn test_metrics_registration() { + let registry = Registry::new(); + register_metrics(®istry).unwrap(); + } + + #[test] + fn test_emit_f3_metrics() { + emit(F3CertificateFetched { + instance: 100, + ec_chain_len: 1, + status: OperationStatus::Success, + latency: 0.5, + }); + + emit(F3CertificateValidated { + instance: 100, + new_instance: 101, + power_table_size: 13, + status: OperationStatus::Success, + latency: 0.1, + }); + } + + #[test] + fn test_emit_proof_metrics() { + emit(ProofBundleGenerated { + highest_epoch: 1000, + storage_proofs: 1, + event_proofs: 2, + witness_blocks: 15, + bundle_size_bytes: 15000, + status: OperationStatus::Success, + latency: 1.2, + }); + + emit(ProofCached { + instance: 100, + cache_size: 5, + highest_cached: 104, + }); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/persistence.rs b/fendermint/vm/topdown/proof-service/src/persistence.rs new file mode 100644 index 0000000000..579528c874 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/persistence.rs @@ -0,0 +1,325 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Persistent storage for proof cache using RocksDB +//! +//! # Why a Separate Database? +//! +//! The proof cache uses its own RocksDB instance for: +//! 1. **Lifecycle Independence**: Can be cleared without affecting chain state +//! 2. **Performance Isolation**: Large proofs don't impact block storage I/O +//! 3. **Operational Flexibility**: Independent backup/restore +//! +//! If cache is wiped, proofs regenerate from parent chain. +//! +//! # Column Families +//! +//! - `metadata`: Schema version +//! - `certificates`: F3 certificates keyed by instance_id +//! - `epoch_proofs`: Proof bundles keyed by epoch + +use crate::types::{CertificateEntry, EpochProofEntry, SerializableCertificateEntry}; +use anyhow::{Context, Result}; +use fvm_shared::clock::ChainEpoch; +use rocksdb::{BoundColumnFamily, Options, DB}; +use std::path::Path; +use std::sync::Arc; +use tracing::{debug, info}; + +/// Database schema version +const SCHEMA_VERSION: u32 = 1; + +/// Column family names +const CF_METADATA: &str = "metadata"; +const CF_CERTIFICATES: &str = "certificates"; +const CF_EPOCH_PROOFS: &str = "epoch_proofs"; + +/// Metadata keys +const KEY_SCHEMA_VERSION: &[u8] = b"schema_version"; + +/// Persistent storage for proof cache +pub struct ProofCachePersistence { + db: Arc, +} + +impl ProofCachePersistence { + /// Open or create a persistent cache at the given path + pub fn open>(path: P) -> Result { + let path = path.as_ref(); + info!(?path, "Opening proof cache database"); + + let mut opts = Options::default(); + opts.create_if_missing(true); + opts.create_missing_column_families(true); + opts.set_compression_type(rocksdb::DBCompressionType::Lz4); + + let cfs = vec![CF_METADATA, CF_CERTIFICATES, CF_EPOCH_PROOFS]; + let db = DB::open_cf(&opts, path, cfs) + .context("Failed to open RocksDB database for proof cache")?; + + let persistence = Self { db: Arc::new(db) }; + persistence.init_schema()?; + + Ok(persistence) + } + + fn get_cf(&self, name: &str) -> Result> { + self.db + .cf_handle(name) + .with_context(|| format!("Failed to get {} column family", name)) + } + + fn init_schema(&self) -> Result<()> { + let cf = self.get_cf(CF_METADATA)?; + + match self.db.get_cf(&cf, KEY_SCHEMA_VERSION)? { + Some(data) => { + let version = serde_json::from_slice::(&data) + .context("Failed to deserialize schema version")?; + if version != SCHEMA_VERSION { + anyhow::bail!( + "Schema version mismatch: found {}, expected {}", + version, + SCHEMA_VERSION + ); + } + info!(version = SCHEMA_VERSION, "Verified schema version"); + } + None => { + self.db.put_cf( + &cf, + KEY_SCHEMA_VERSION, + serde_json::to_vec(&SCHEMA_VERSION)?, + )?; + info!(version = SCHEMA_VERSION, "Initialized new schema"); + } + } + + Ok(()) + } + + pub fn save_certificate(&self, entry: &CertificateEntry) -> Result<()> { + let cf = self.get_cf(CF_CERTIFICATES)?; + let key = entry.instance_id().to_be_bytes(); + let value = serde_json::to_vec(&SerializableCertificateEntry::from(entry)) + .context("Failed to serialize certificate entry")?; + + self.db.put_cf(&cf, key, value)?; + debug!( + instance_id = entry.instance_id(), + "Saved certificate to disk" + ); + Ok(()) + } + + pub fn load_all_certificates(&self) -> Result> { + let cf = self.get_cf(CF_CERTIFICATES)?; + let mut entries = Vec::new(); + + for item in self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start) { + let (_, value) = item?; + let entry: SerializableCertificateEntry = serde_json::from_slice(&value) + .context("Failed to deserialize certificate entry")?; + entries.push(CertificateEntry::try_from(entry)?); + } + + info!(count = entries.len(), "Loaded certificates from disk"); + Ok(entries) + } + + pub fn delete_certificate(&self, instance_id: u64) -> Result<()> { + let cf = self.get_cf(CF_CERTIFICATES)?; + self.db.delete_cf(&cf, instance_id.to_be_bytes())?; + debug!(instance_id, "Deleted certificate from disk"); + Ok(()) + } + + pub fn save_epoch_proof(&self, entry: &EpochProofEntry) -> Result<()> { + let cf = self.get_cf(CF_EPOCH_PROOFS)?; + let key = entry.epoch.to_be_bytes(); + let value = serde_json::to_vec(entry).context("Failed to serialize epoch proof entry")?; + + self.db.put_cf(&cf, key, value)?; + debug!(epoch = entry.epoch, "Saved epoch proof to disk"); + Ok(()) + } + + pub fn load_all_epoch_proofs(&self) -> Result> { + let cf = self.get_cf(CF_EPOCH_PROOFS)?; + let mut entries = Vec::new(); + + for item in self.db.iterator_cf(&cf, rocksdb::IteratorMode::Start) { + let (_, value) = item?; + let entry: EpochProofEntry = serde_json::from_slice(&value) + .context("Failed to deserialize epoch proof entry")?; + entries.push(entry); + } + + info!(count = entries.len(), "Loaded epoch proofs from disk"); + Ok(entries) + } + + pub fn delete_epoch_proof(&self, epoch: ChainEpoch) -> Result<()> { + let cf = self.get_cf(CF_EPOCH_PROOFS)?; + self.db.delete_cf(&cf, epoch.to_be_bytes())?; + debug!(epoch, "Deleted epoch proof from disk"); + Ok(()) + } + + pub fn clear_all(&self) -> Result<()> { + self.clear_cf(CF_CERTIFICATES)?; + self.clear_cf(CF_EPOCH_PROOFS)?; + debug!("Cleared all cache entries from disk"); + Ok(()) + } + + fn clear_cf(&self, cf_name: &str) -> Result<()> { + if let Some(cf) = self.db.cf_handle(cf_name) { + let keys: Vec> = self + .db + .iterator_cf(&cf, rocksdb::IteratorMode::Start) + .filter_map(|r| r.ok().map(|(k, _)| k)) + .collect(); + for key in keys { + self.db.delete_cf(&cf, &key)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + SerializableCertificateEntry, SerializableECChainEntry, SerializableF3Certificate, + SerializablePowerEntries, SerializablePowerEntry, SerializableSupplementalData, + }; + use cid::Cid; + use multihash_codetable::{Code, MultihashDigest}; + use proofs::proofs::common::bundle::UnifiedProofBundle; + use std::time::SystemTime; + use tempfile::tempdir; + + fn create_test_certificate(instance_id: u64) -> CertificateEntry { + let power_table_cid = Cid::new_v1(0x55, Code::Blake2b256.digest(b"test")); + + let ec_chain = (100..=102) + .map(|epoch| SerializableECChainEntry { + epoch, + key: vec!["0".to_string()], + power_table: power_table_cid.to_string(), + commitments: vec![0u8; 32], + }) + .collect(); + + let serializable = SerializableCertificateEntry { + certificate: SerializableF3Certificate { + gpbft_instance: instance_id, + ec_chain, + supplemental_data: SerializableSupplementalData { + power_table: power_table_cid.to_string(), + 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], + }]), + source_rpc: "test".to_string(), + fetched_at: SystemTime::now(), + }; + + CertificateEntry::try_from(serializable).expect("valid certificate entry") + } + + fn create_test_epoch_proof(epoch: ChainEpoch) -> EpochProofEntry { + EpochProofEntry::new( + epoch, + UnifiedProofBundle { + storage_proofs: vec![], + event_proofs: vec![], + blocks: vec![], + }, + 5, // cert_instance + ) + } + + #[test] + fn test_persistence_certificates() { + let dir = tempdir().unwrap(); + let persistence = ProofCachePersistence::open(dir.path()).unwrap(); + + // Save certificates + let cert1 = create_test_certificate(100); + let cert2 = create_test_certificate(101); + persistence.save_certificate(&cert1).unwrap(); + persistence.save_certificate(&cert2).unwrap(); + + // Load all + let loaded = persistence.load_all_certificates().unwrap(); + assert_eq!(loaded.len(), 2); + } + + #[test] + fn test_persistence_epoch_proofs() { + let dir = tempdir().unwrap(); + let persistence = ProofCachePersistence::open(dir.path()).unwrap(); + + // Save epoch proofs + for epoch in 100..105 { + persistence + .save_epoch_proof(&create_test_epoch_proof(epoch)) + .unwrap(); + } + + // Load all + let loaded = persistence.load_all_epoch_proofs().unwrap(); + assert_eq!(loaded.len(), 5); + } + + #[test] + fn test_persistence_delete() { + let dir = tempdir().unwrap(); + let persistence = ProofCachePersistence::open(dir.path()).unwrap(); + + // Save and delete certificate + persistence + .save_certificate(&create_test_certificate(100)) + .unwrap(); + persistence.delete_certificate(100).unwrap(); + let certs = persistence.load_all_certificates().unwrap(); + assert_eq!(certs.len(), 0); + + // Save and delete epoch proof + persistence + .save_epoch_proof(&create_test_epoch_proof(200)) + .unwrap(); + persistence.delete_epoch_proof(200).unwrap(); + let proofs = persistence.load_all_epoch_proofs().unwrap(); + assert_eq!(proofs.len(), 0); + } + + #[test] + fn test_persistence_clear_all() { + let dir = tempdir().unwrap(); + let persistence = ProofCachePersistence::open(dir.path()).unwrap(); + + // Save some data + persistence + .save_certificate(&create_test_certificate(100)) + .unwrap(); + persistence + .save_epoch_proof(&create_test_epoch_proof(200)) + .unwrap(); + + // Clear all + persistence.clear_all().unwrap(); + + assert_eq!(persistence.load_all_certificates().unwrap().len(), 0); + assert_eq!(persistence.load_all_epoch_proofs().unwrap().len(), 0); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/service.rs b/fendermint/vm/topdown/proof-service/src/service.rs new file mode 100644 index 0000000000..88271f31fa --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/service.rs @@ -0,0 +1,340 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Proof generator service - orchestrates proof generation pipeline +//! +//! # Architecture +//! +//! Each F3 certificate contains tipsets [base, suffix...] where: +//! - `base` = last finalized epoch from previous certificate (overlap point) +//! - `suffix` = new epochs being finalized +//! +//! For each certificate, we generate proofs for all (parent, child) pairs: +//! - Given [E0, E1, E2, E3], we prove E0, E1, E2 (E3 has no child yet) +//! - E3 will be proven when next certificate arrives (as its base) +//! +//! Each proof requires both parent (epoch E) and child (typically epoch E+1) because +//! Filecoin stores `parentReceipts` in the child block, not the parent. + +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 anyhow::{Context, Result}; +use filecoin_f3_certs::FinalityCertificate; +use filecoin_f3_gpbft::PowerEntries; +use ipc_api::subnet_id::SubnetID; +use std::sync::Arc; +use tokio::time::{interval, MissedTickBehavior}; + +/// Main proof generator service +pub struct ProofGeneratorService { + config: ProofServiceConfig, + cache: Arc, + f3_client: F3Client, + assembler: ProofAssembler, +} + +impl ProofGeneratorService { + /// Create a new proof generator service + /// + /// # Arguments + /// * `config` - Service configuration + /// * `cache` - Proof cache + /// * `subnet_id` - id of the subnet to prove + /// * `initial_instance` - F3 instance to bootstrap from (from F3CertManager actor) + /// * `initial_power_table` - Initial power table (from F3CertManager actor) + /// + /// Both `initial_instance` and `initial_power_table` should come from the F3CertManager + /// actor on-chain, which holds the last committed certificate and its power table. + pub async fn new( + config: ProofServiceConfig, + cache: Arc, + subnet_id: &SubnetID, + initial_instance: u64, + initial_power_table: PowerEntries, + ) -> Result { + let gateway_actor_id = extract_gateway_actor_id_from_config(&config).await?; + + // Get the current highest instance from the cache + let highest_cached_instance = cache.highest_cached_instance(); + + let (start_instance, start_power_table) = if let Some(cached) = highest_cached_instance { + if cached > initial_instance { + tracing::info!( + highest_cached_instance = cached, + initial_instance, + "Using cached instance instead of initial instance" + ); + + let cert_entry = cache + .get_certificate(cached) + .context("Failed to get cached certificate")?; + (cached, cert_entry.power_table) + } else { + (initial_instance, initial_power_table) + } + } else { + (initial_instance, initial_power_table) + }; + + // Create F3 client for certificate fetching + validation + let f3_client = F3Client::new( + &config.parent_rpc_url, + &config.f3_network_name(subnet_id), + start_instance, + start_power_table, + ) + .context("Failed to create F3 client")?; + + // Create proof assembler + let assembler = ProofAssembler::new( + config.parent_rpc_url.clone(), + gateway_actor_id, + subnet_id.to_string(), + ) + .context("Failed to create proof assembler")?; + + Ok(Self { + config, + cache, + f3_client, + assembler, + }) + } + + /// Main service loop - runs continuously and polls parent chain periodically + /// + /// Each tick processes ONE certificate (if needed and available). + /// The ticker acts as the outer loop - no inner loop needed. + /// Errors are logged but don't stop the service - it will retry on next tick. + pub async fn run(mut self) { + tracing::info!( + polling_interval = ?self.config.polling_interval, + lookahead_instances = self.config.cache_config.lookahead_instances, + "Starting proof generator service" + ); + + let mut poll_interval = interval(self.config.polling_interval); + poll_interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + poll_interval.tick().await; + + if let Err(e) = self.process_next_certificate().await { + tracing::error!( + error = %e, + "Failed to process certificate, will retry on next tick" + ); + } + } + } + + /// Process next certificate if we haven't reached the lookahead target. + /// + /// This is the main tick handler - processes at most one certificate per call. + /// The ticker in `run()` provides the outer loop. + /// + /// # Future improvements + /// TODO: Gap recovery could be added when multiple RPC endpoints are available. + async fn process_next_certificate(&mut self) -> Result<()> { + if !self.should_fetch_more() { + return Ok(()); + } + + let Some((certificate, power_table)) = self.fetch_next_certificate().await? else { + return Ok(()); // No certificate available, caught up with F3 + }; + + self.generate_proofs_for_certificate(&certificate, &power_table) + .await?; + + Ok(()) + } + + /// Check if we should fetch more certificates based on lookahead. + fn should_fetch_more(&self) -> bool { + let current_instance = self.f3_client.current_instance(); + let last_committed = self.cache.last_committed_instance(); + let lookahead = self.config.cache_config.lookahead_instances; + let target = last_committed + lookahead; + + if current_instance >= target { + tracing::debug!( + current_instance, + last_committed, + target, + "Already at lookahead target, nothing to do" + ); + false + } else { + true + } + } + + /// Fetch and validate the next certificate from F3. + /// Returns `None` if no certificate is available (caught up). + async fn fetch_next_certificate( + &mut self, + ) -> Result> { + match self.f3_client.fetch_and_validate().await { + Ok((cert, power_table)) => { + self.log_certificate(&cert); + Ok(Some((cert, power_table))) + } + Err(err) if is_certificate_unavailable(&err) => { + tracing::debug!("Caught up with F3 - no more certificates available"); + Ok(None) + } + Err(err) => Err(err).context("Failed to fetch and validate certificate"), + } + } + + /// Log certificate info for debugging. + fn log_certificate(&self, cert: &FinalityCertificate) { + let suffix_epochs: Vec = cert.ec_chain.suffix().iter().map(|ts| ts.epoch).collect(); + tracing::info!( + instance = cert.gpbft_instance, + base_epoch = ?cert.ec_chain.base().map(|b| b.epoch), + suffix_epochs = ?suffix_epochs, + "Fetched and validated certificate" + ); + } + + /// Generate proofs for all (parent, child) tipset pairs in the certificate. + /// + /// Each proof requires both the parent tipset (epoch E) and child tipset (typically epoch E+1). + /// The child contains `parentReceipts` which commits to the parent's execution results. + /// + /// Given tipsets [E0, E1, E2, E3], we generate proofs for: + /// - E0 (using E1 as child) + /// - E1 (using E2 as child) + /// - E2 (using E3 as child) + /// - E3 has no child in this certificate, will be proven with next certificate + async fn generate_proofs_for_certificate( + &self, + cert: &FinalityCertificate, + power_table: &PowerEntries, + ) -> Result<()> { + // Build (parent, child) pairs using windows - this makes the requirement explicit + let tipset_pairs: Vec<_> = cert + .ec_chain + .iter() + .map(FinalizedTipset::from) + .collect::>() + .windows(2) + .map(|w| (w[0].clone(), w[1].clone())) + .collect(); + + if tipset_pairs.is_empty() { + tracing::debug!( + instance = cert.gpbft_instance, + "Certificate has fewer than 2 tipsets, no (parent, child) pairs to prove" + ); + return Ok(()); + } + + let epochs_to_prove: Vec = tipset_pairs.iter().map(|(p, _)| p.epoch).collect(); + + tracing::info!( + instance = cert.gpbft_instance, + epochs = ?epochs_to_prove, + "Generating proofs for certificate epochs" + ); + + let mut epoch_proofs = Vec::with_capacity(tipset_pairs.len()); + + // Generate proofs for each (parent, child) pair + // The child tipset contains `parentReceipts` which commits to the parent's execution. + for (parent_tipset, child_tipset) in tipset_pairs { + let parent_epoch = parent_tipset.epoch; + + tracing::debug!( + parent_epoch, + child_epoch = child_tipset.epoch, + "Generating proof for epoch" + ); + + let proof_bundle = self + .assembler + .generate_proof_for_epoch(parent_tipset.clone(), child_tipset.clone()) + .await + .with_context(|| format!("Failed to generate proof for epoch {}", parent_epoch))?; + + epoch_proofs.push(EpochProofEntry::new( + parent_epoch, + proof_bundle, + cert.gpbft_instance, + )); + } + + // Cache the certificate and proofs + let rpc_endpoint = self.f3_client.rpc_endpoint().to_string(); + let cert_entry = CertificateEntry::new(cert.clone(), power_table.clone(), rpc_endpoint); + self.cache.insert_certificate(cert_entry)?; + self.cache.insert_epoch_proofs(epoch_proofs)?; + + tracing::info!( + epoch_count = epochs_to_prove.len(), + "Successfully generated and cached proofs" + ); + + Ok(()) + } + + /// Get reference to the cache (for proposers) + pub fn cache(&self) -> &Arc { + &self.cache + } +} + +/// Check if an error indicates the certificate is not yet available. +fn is_certificate_unavailable(err: &anyhow::Error) -> bool { + let msg = err.to_string(); + msg.contains("not found") || msg.contains("not available") +} + +async fn extract_gateway_actor_id_from_config(config: &ProofServiceConfig) -> Result { + match &config.gateway_id { + GatewayId::ActorId(id) => Ok(*id), + GatewayId::EthAddress(eth_addr) => { + resolve_eth_address_to_actor_id(eth_addr, &config.parent_rpc_url).await + } + } +} + +async fn resolve_eth_address_to_actor_id(eth_addr: &str, parent_rpc_url: &str) -> Result { + let client = proofs::client::LotusClient::new(url::Url::parse(parent_rpc_url)?, None); + let actor_id = proofs::proofs::resolve_eth_address_to_actor_id(&client, eth_addr) + .await + .with_context(|| format!("Failed to resolve gateway Ethereum address: {}", eth_addr))?; + Ok(actor_id) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::CacheConfig; + + #[tokio::test] + async fn test_service_creation() { + use filecoin_f3_gpbft::PowerEntries; + + let config = ProofServiceConfig { + enabled: true, + parent_rpc_url: "http://localhost:1234/rpc/v1".to_string(), + gateway_id: GatewayId::ActorId(1001), + cache_config: CacheConfig::default(), + ..Default::default() + }; + + let cache = Arc::new(ProofCache::new(100, 0, config.cache_config.clone())); + let power_table = PowerEntries(vec![]); + let subnet_id = SubnetID::default(); + + // Note: Service creation succeeds with F3Client::new() even with a fake RPC endpoint + // The actual RPC calls will fail later when the service tries to fetch certificates + let result = ProofGeneratorService::new(config, cache, &subnet_id, 0, power_table).await; + assert!(result.is_ok()); + } +} diff --git a/fendermint/vm/topdown/proof-service/src/types.rs b/fendermint/vm/topdown/proof-service/src/types.rs new file mode 100644 index 0000000000..b87b2c9244 --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/types.rs @@ -0,0 +1,548 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Types for the proof generator service + +use anyhow::{bail, Context, Result}; +use filecoin_f3_certs::{FinalityCertificate, PowerTableDelta, PowerTableDiff}; +use filecoin_f3_gpbft::{self, Cid, ECChain, PowerEntries, PowerEntry, SupplementalData, Tipset}; +use fvm_ipld_bitfield::BitField; +use fvm_shared::clock::ChainEpoch; +use keccak_hash::H256; +use num_bigint::BigInt; +use proofs::proofs::common::bundle::UnifiedProofBundle; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::time::SystemTime; + +/// Parse a 32-byte slice into an H256 hash +fn parse_commitments(bytes: &[u8]) -> Result { + if bytes.len() != 32 { + bail!("Commitments must be exactly 32 bytes, got {}", bytes.len()); + } + Ok(H256::from_slice(bytes)) +} + +/// Parse a string as a BigInt with context +fn parse_bigint(s: &str, context: &str) -> Result { + s.parse::() + .with_context(|| format!("Invalid BigInt for {}: {}", context, s)) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct FinalizedTipsets(Vec); + +impl Deref for FinalizedTipsets { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FinalizedTipsets { + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn last(&self) -> Option<&FinalizedTipset> { + self.0.last() + } + + /// Merge two ECChains into a single FinalizedTipsets + pub fn merge(a: &ECChain, b: &ECChain) -> Self { + Self( + a.iter() + .chain(b.iter()) + .map(FinalizedTipset::from) + .collect(), + ) + } +} + +impl From<&[Tipset]> for FinalizedTipsets { + /// Convert from slice of F3 Tipsets + fn from(tipsets: &[Tipset]) -> Self { + Self(tipsets.iter().map(FinalizedTipset::from).collect()) + } +} + +impl From<&ECChain> for FinalizedTipsets { + /// Convert from F3 ECChain + fn from(ec_chain: &ECChain) -> Self { + Self(ec_chain.iter().map(FinalizedTipset::from).collect()) + } +} + +impl TryFrom<&[proofs::client::types::ApiTipset]> for FinalizedTipsets { + type Error = anyhow::Error; + + /// Convert from slice of ApiTipsets + fn try_from(tipsets: &[proofs::client::types::ApiTipset]) -> Result { + tipsets + .iter() + .map(FinalizedTipset::try_from) + .collect::>>() + .map(Self) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct FinalizedTipset { + /// The epoch of the tipset + pub epoch: i64, + /// Canonically ordered concatenated block-header CIDs + pub block_cids: Vec, +} + +impl FinalizedTipset { + /// Verify this tipset matches another (e.g., fetched from RPC) + /// + /// Returns an error with details if they don't match. + pub fn verify_matches(&self, other: &Self) -> Result<()> { + if self.epoch != other.epoch || self.block_cids != other.block_cids { + bail!( + "Tipset mismatch: expected (epoch={}, cids={:x?}) got (epoch={}, cids={:x?})", + self.epoch, + self.block_cids, + other.epoch, + other.block_cids + ); + } + Ok(()) + } +} + +impl From<&Tipset> for FinalizedTipset { + /// Convert from F3 library's Tipset. + /// The key field is already concatenated bytes. + fn from(tipset: &Tipset) -> Self { + Self { + epoch: tipset.epoch, + block_cids: tipset.key.clone(), + } + } +} + +impl TryFrom<&proofs::client::types::ApiTipset> for FinalizedTipset { + type Error = anyhow::Error; + + /// Convert from proofs library's ApiTipset. + /// Follows F3's convert_tipset_key pattern. + fn try_from(api_tipset: &proofs::client::types::ApiTipset) -> Result { + let mut block_cids = Vec::new(); + for cid_map in &api_tipset.cids { + let cid = Cid::try_from(cid_map.cid.as_str())?; + block_cids.extend(cid.to_bytes()); + } + Ok(Self { + epoch: api_tipset.height, + block_cids, + }) + } +} + +/// Serializable EC Chain entry +/// +/// Represents a single tipset in the finalized chain. +/// Matches the structure from filecoin_f3_gpbft::TipSet +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializableECChainEntry { + /// Tipset epoch + pub epoch: ChainEpoch, + /// Tipset key (CIDs as strings for serialization) + pub key: Vec, + /// Power table CID (as string for serialization) + pub power_table: String, + /// Commitments (32-byte hash as bytes) + pub commitments: Vec, +} + +impl SerializableECChainEntry { + fn into_tipset(self) -> Result { + let key = self + .key + .into_iter() + .map(|byte| { + byte.parse::() + .with_context(|| format!("Invalid tipset key byte: {}", byte)) + }) + .collect::>>()?; + + let power_table = self + .power_table + .parse::() + .context("Invalid power table CID in ECChain entry")?; + + let commitments = parse_commitments(&self.commitments)?; + + Ok(Tipset { + epoch: self.epoch, + key, + power_table, + commitments, + }) + } +} + +/// Serializable supplemental data +/// +/// Matches the structure from filecoin_f3_gpbft::SupplementalData +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializableSupplementalData { + /// Power table CID (as string for serialization) + pub power_table: String, + /// Commitments (32-byte hash as bytes) + pub commitments: Vec, +} + +impl SerializableSupplementalData { + fn into_supplemental_data(self) -> Result { + let commitments = parse_commitments(&self.commitments)?; + let power_table = self + .power_table + .parse::() + .context("Invalid power table CID in supplemental data")?; + + Ok(SupplementalData { + commitments, + power_table, + }) + } +} + +/// Serializable power table delta entry +/// +/// Matches the structure from filecoin_f3_gpbft::PowerTableDelta +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializablePowerTableDelta { + /// Participant ID + pub participant_id: u64, + /// Power delta as string (signed - can be negative for decreases) + pub power_delta: String, + /// Signing key (public key bytes) + pub signing_key: Vec, +} + +impl SerializablePowerTableDelta { + fn into_power_table_delta(self) -> Result { + let power_delta = parse_bigint( + &self.power_delta, + &format!("participant {}", self.participant_id), + )?; + + Ok(PowerTableDelta { + participant_id: self.participant_id, + power_delta, + signing_key: filecoin_f3_gpbft::PubKey(self.signing_key), + }) + } +} + +/// Serializable power table entry +/// +/// Matches the structure from filecoin_f3_gpbft::PowerEntry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializablePowerEntry { + /// Validator ID + pub id: u64, + /// Power/weight as string (BigInt) + pub power: String, + /// Public key bytes + pub pub_key: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializablePowerEntries(pub Vec); + +impl SerializablePowerEntry { + fn into_power_entry(self) -> Result { + let power = parse_bigint(&self.power, &format!("participant {}", self.id))?; + + Ok(PowerEntry { + id: self.id, + power, + pub_key: filecoin_f3_gpbft::PubKey(self.pub_key), + }) + } +} + +impl SerializablePowerEntries { + pub fn into_power_entries(self) -> Result { + let entries = self + .0 + .into_iter() + .map(|entry| entry.into_power_entry()) + .collect::>>()?; + Ok(PowerEntries(entries)) + } +} + +/// Serializable F3 certificate for cache storage and transaction inclusion +/// +/// Contains essential validated certificate data in a format that can be: +/// - Serialized for RocksDB persistence +/// - Included in consensus transactions +/// - Used for proof verification +/// +/// This structure matches filecoin_f3_certs::FinalityCertificate field names +/// exactly, but uses serializable types. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct SerializableF3Certificate { + /// The GPBFT instance to which this finality certificate corresponds + /// Matches: FinalityCertificate.gpbft_instance + pub gpbft_instance: u64, + + /// The ECChain finalized during this instance + /// Matches: FinalityCertificate.ec_chain + /// Structure: [base, suffix...] + /// - base: last tipset finalized in previous instance + /// - suffix: new tipsets being finalized in this instance (may be empty) + pub ec_chain: Vec, + + /// Additional data signed by the participants in this instance + /// Matches: FinalityCertificate.supplemental_data + pub supplemental_data: SerializableSupplementalData, + + /// Indexes in the base power table of the certifiers (bitfield) + /// Matches: FinalityCertificate.signers + pub signers: Vec, + + /// Aggregated signature of the certifiers + /// Matches: FinalityCertificate.signature + pub signature: Vec, + + /// Changes between the power table used to validate this finality certificate + /// and the power table used to validate the next finality certificate + /// Matches: FinalityCertificate.power_table_delta + pub power_table_delta: Vec, +} + +impl SerializableF3Certificate { + /// Get all finalized epochs from the ec_chain + /// + /// Returns epochs from both base and suffix tipsets + pub fn finalized_epochs(&self) -> Vec { + self.ec_chain.iter().map(|entry| entry.epoch).collect() + } + + pub fn try_into_certificate(self) -> Result { + let tipsets = self + .ec_chain + .into_iter() + .map(|entry| entry.into_tipset()) + .collect::>>()?; + let ec_chain = ECChain::new_unvalidated(tipsets); + + ec_chain.validate().context("Failed to validate EC chain")?; + + let supplemental_data = self.supplemental_data.into_supplemental_data()?; + let signers = BitField::try_from_bits(self.signers.iter().copied()) + .context("Failed to rebuild signers bitfield")?; + let power_table_delta = self + .power_table_delta + .into_iter() + .map(|delta| delta.into_power_table_delta()) + .collect::>()?; + + Ok(FinalityCertificate { + gpbft_instance: self.gpbft_instance, + ec_chain, + supplemental_data, + signers, + signature: self.signature, + power_table_delta, + }) + } +} + +impl From<&FinalityCertificate> for SerializableF3Certificate { + fn from(cert: &FinalityCertificate) -> Self { + // Convert EC chain to serializable format + let ec_chain = cert + .ec_chain + .iter() + .map(|ts| SerializableECChainEntry { + epoch: ts.epoch, + key: ts.key.iter().map(|cid| cid.to_string()).collect(), + power_table: ts.power_table.to_string(), + commitments: ts.commitments.as_bytes().to_vec(), + }) + .collect(); + + // Convert supplemental data + let supplemental_data = SerializableSupplementalData { + power_table: cert.supplemental_data.power_table.to_string(), + commitments: cert.supplemental_data.commitments.as_bytes().to_vec(), + }; + + // Convert power table delta + let power_table_delta = cert + .power_table_delta + .iter() + .map(|delta| SerializablePowerTableDelta { + participant_id: delta.participant_id, + power_delta: delta.power_delta.to_string(), + signing_key: delta.signing_key.0.clone(), + }) + .collect(); + + Self { + gpbft_instance: cert.gpbft_instance, + ec_chain, + supplemental_data, + signers: cert.signers.iter().collect(), + signature: cert.signature.clone(), + power_table_delta, + } + } +} + +impl From<&PowerEntry> for SerializablePowerEntry { + fn from(entry: &PowerEntry) -> Self { + Self { + id: entry.id, + power: entry.power.to_string(), + pub_key: entry.pub_key.0.clone(), + } + } +} + +impl From<&PowerEntries> for SerializablePowerEntries { + fn from(entries: &PowerEntries) -> Self { + Self(entries.iter().map(SerializablePowerEntry::from).collect()) + } +} + +/// Entry in the epoch proof cache (keyed by epoch) +/// +/// This is the primary cache entry that consumers will query. +/// It contains the proof for a single epoch and references to the +/// certificates needed for verification. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EpochProofEntry { + /// The chain epoch at which the storage modifications has happened and events were emitted + pub epoch: ChainEpoch, + + /// The proof bundle for this epoch + pub proof_bundle: UnifiedProofBundle, + + /// Instance ID of the certificate that contains both this and the next tipset's epoch + pub cert_instance: u64, + + /// Metadata + pub generated_at: SystemTime, +} + +impl EpochProofEntry { + pub fn new(epoch: ChainEpoch, proof_bundle: UnifiedProofBundle, cert_instance: u64) -> Self { + Self { + epoch, + proof_bundle, + cert_instance, + generated_at: SystemTime::now(), + } + } +} + +/// Certificate entry for the certificate store (keyed by instance ID) +/// +/// Certificates are stored separately to avoid duplication when multiple +/// epochs reference the same certificate. +#[derive(Debug, Clone)] +pub struct CertificateEntry { + /// The validated F3 certificate + pub certificate: FinalityCertificate, + + /// Power table after applying this certificate's power_table_delta + pub power_table: PowerEntries, + + /// Source RPC endpoint + pub source_rpc: String, + + /// When this certificate was fetched + pub fetched_at: SystemTime, +} + +impl CertificateEntry { + pub fn new( + certificate: FinalityCertificate, + power_table: PowerEntries, + source_rpc: String, + ) -> Self { + Self { + certificate, + power_table, + source_rpc, + fetched_at: SystemTime::now(), + } + } + + /// Get the instance ID of this certificate + pub fn instance_id(&self) -> u64 { + self.certificate.gpbft_instance + } +} + +/// Serializable version of CertificateEntry for disk persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializableCertificateEntry { + pub certificate: SerializableF3Certificate, + pub power_table: SerializablePowerEntries, + pub source_rpc: String, + pub fetched_at: SystemTime, +} + +impl From<&CertificateEntry> for SerializableCertificateEntry { + fn from(entry: &CertificateEntry) -> Self { + Self { + certificate: SerializableF3Certificate::from(&entry.certificate), + power_table: SerializablePowerEntries::from(&entry.power_table), + source_rpc: entry.source_rpc.clone(), + fetched_at: entry.fetched_at, + } + } +} + +impl TryFrom for CertificateEntry { + type Error = anyhow::Error; + + fn try_from(entry: SerializableCertificateEntry) -> Result { + Ok(Self { + certificate: entry.certificate.try_into_certificate()?, + power_table: entry.power_table.into_power_entries()?, + source_rpc: entry.source_rpc, + fetched_at: entry.fetched_at, + }) + } +} + +/// Result of looking up an epoch proof with its certificates +/// +/// This is what consumers receive when they query for an epoch's proof. +/// It includes everything needed for verification. +#[derive(Debug, Clone)] +pub struct EpochProofWithCertificate { + /// The chain epoch at which the storage modifications has happened and events were emitted + pub epoch: ChainEpoch, + + /// The proof bundle + pub proof_bundle: UnifiedProofBundle, + + /// The certificate that contains both this and the next tipset's epoch + pub certificate: FinalityCertificate, + + pub finalized_tipsets: FinalizedTipsets, +} + +impl EpochProofWithCertificate { + /// Create from an epoch proof entry and its referenced certificate + pub fn new(proof_entry: &EpochProofEntry, cert_entry: &CertificateEntry) -> Self { + let finalized_tipsets = FinalizedTipsets::from(&cert_entry.certificate.ec_chain); + Self { + epoch: proof_entry.epoch, + proof_bundle: proof_entry.proof_bundle.clone(), + certificate: cert_entry.certificate.clone(), + finalized_tipsets, + } + } +} diff --git a/fendermint/vm/topdown/proof-service/src/verifier.rs b/fendermint/vm/topdown/proof-service/src/verifier.rs new file mode 100644 index 0000000000..1cae49132f --- /dev/null +++ b/fendermint/vm/topdown/proof-service/src/verifier.rs @@ -0,0 +1,146 @@ +// Copyright 2022-2025 Protocol Labs +// SPDX-License-Identifier: Apache-2.0, MIT +//! Proof bundle verification for block attestation +//! +//! Provides deterministic verification of proof bundles against F3 certificates. +//! Used by validators during block attestation to verify parent finality proofs. +//! +//! # Verification Flow +//! +//! The verifier checks that witness blocks in the proof bundle are certified +//! by the F3 certificates. With the two-level cache design, proofs are verified +//! against pre-merged tipsets from both the parent and child certificates. + +use crate::assembler::{NEW_POWER_CHANGE_REQUEST_SIGNATURE, NEW_TOPDOWN_MESSAGE_SIGNATURE}; +use crate::types::{EpochProofWithCertificate, FinalizedTipsets}; +use anyhow::Result; +use cid::Cid; +use proofs::proofs::common::bundle::{UnifiedProofBundle, UnifiedVerificationResult}; +use proofs::proofs::events::bundle::EventProofBundle; +use proofs::proofs::events::verifier::verify_event_proof; +use proofs::proofs::storage::verifier::verify_storage_proof; + +use proofs::proofs::common::evm::{ascii_to_bytes32, extract_evm_log, hash_event_signature}; + +pub struct ProofVerifier { + events: Vec>, +} + +impl ProofVerifier { + pub fn new(subnet_id: String) -> Self { + let events = vec![ + vec![ + hash_event_signature(NEW_TOPDOWN_MESSAGE_SIGNATURE), + ascii_to_bytes32(&subnet_id), + ], + vec![hash_event_signature(NEW_POWER_CHANGE_REQUEST_SIGNATURE)], + ]; + + Self { events } + } + + /// Verify a inclusion proof in the proof bundle using pre-merged tipsets from certificates + /// + /// This is the primary verification method. It verifies that all witness + /// blocks in the proof bundle are certified by the provided tipsets. + /// + /// # Arguments + /// * `bundle` - The proof bundle to verify + /// * `merged_tipsets` - Pre-merged tipsets from parent and child certificates + /// + /// # Returns + /// Verification results for storage and event inclusion proofs + pub fn verify_proof_bundle_with_tipsets( + &self, + bundle: &UnifiedProofBundle, + finalized_tipsets: &FinalizedTipsets, + ) -> Result { + let tipset_verifier = |epoch: i64, cid: &Cid| -> bool { + finalized_tipsets + .iter() + .any(|ts| ts.epoch == epoch && ts.block_cids == cid.to_bytes()) + }; + + self.verify_with_verifier(bundle, &tipset_verifier) + } + + /// Verify a proof bundle from a cache entry + /// + /// # Arguments + /// * `entry` - The epoch proof entry with its certificates + /// + /// # Returns + /// Verification results for storage and event proofs + pub fn verify_epoch_proof( + &self, + entry: &EpochProofWithCertificate, + ) -> Result { + self.verify_proof_bundle_with_tipsets(&entry.proof_bundle, &entry.finalized_tipsets) + } + + /// Internal verification using a tipset verifier closure + fn verify_with_verifier( + &self, + bundle: &UnifiedProofBundle, + tipset_verifier: &F, + ) -> Result + where + F: Fn(i64, &Cid) -> bool, + { + // Verify storage proofs + let mut storage_results = Vec::new(); + for proof in &bundle.storage_proofs { + let result = verify_storage_proof(proof, &bundle.blocks, tipset_verifier)?; + storage_results.push(result); + } + + // Verify event proofs + let event_bundle = EventProofBundle { + proofs: bundle.event_proofs.clone(), + blocks: bundle.blocks.clone(), + }; + + let parent_tipset_verifier = |epoch: i64, cids: &[Cid]| -> bool { + cids.iter().all(|cid| tipset_verifier(epoch, cid)) + }; + + let event_results = verify_event_proof( + &event_bundle, + &parent_tipset_verifier, + tipset_verifier, + Some(&self.create_event_filter()), + )?; + + Ok(UnifiedVerificationResult { + storage_results, + event_results, + }) + } + + fn create_event_filter(&self) -> impl Fn(&fvm_shared::event::ActorEvent) -> bool + '_ { + |ev: &fvm_shared::event::ActorEvent| -> bool { + if let Some(log) = extract_evm_log(ev) { + self.events.iter().any(|expected_topics| { + log.topics.len() >= expected_topics.len() + && expected_topics + .iter() + .zip(log.topics.iter()) + .all(|(expected, actual)| expected == actual) + }) + } else { + false + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_verifier_creation() { + let verifier = ProofVerifier::new("test-subnet".to_string()); + assert_eq!(verifier.events.len(), 2); + } +} diff --git a/ipc/provider/Cargo.toml b/ipc/provider/Cargo.toml index ddc8ecd972..f2c19911bd 100644 --- a/ipc/provider/Cargo.toml +++ b/ipc/provider/Cargo.toml @@ -54,13 +54,14 @@ ipc-api = { path = "../../ipc/api" } ipc_actors_abis = { path = "../../contract-bindings" } ipc-observability = { path = "../../ipc/observability" } prometheus = { workspace = true } -tendermint-rpc = { workspace = true } -tendermint = { workspace = true } +tendermint-rpc = { workspace = true } +tendermint = { workspace = true } 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 } diff --git a/ipc/wallet/Cargo.toml b/ipc/wallet/Cargo.toml index 1b824f520e..ef4e3c2b49 100644 --- a/ipc/wallet/Cargo.toml +++ b/ipc/wallet/Cargo.toml @@ -13,7 +13,7 @@ anyhow = { workspace = true } argon2 = "0.5" base64 = { workspace = true } blake2b_simd = { workspace = true } -bls-signatures = { version = "0.13.1", default-features = false, features = [ +bls-signatures = { version = "0.15", default-features = false, features = [ "blst", ] } ethers = { workspace = true, optional = true } diff --git a/ipld/resolver/tests/smoke.rs b/ipld/resolver/tests/smoke.rs index db28e7c71a..d15a8c3dff 100644 --- a/ipld/resolver/tests/smoke.rs +++ b/ipld/resolver/tests/smoke.rs @@ -342,8 +342,8 @@ fn make_config(rng: &mut StdRng, cluster_size: u32, bootstrap_addr: Option