diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02381dfc..f10532a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -225,7 +225,7 @@ jobs: - uses: ./.github/actions/install-risc0 - name: Install just - run: cargo install just + run: cargo install --locked just - name: Build artifacts run: just build-artifacts diff --git a/Cargo.lock b/Cargo.lock index 304aaf49..95e489fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -627,6 +627,51 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "asn1_der" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" + [[package]] name = "astral-tokio-tar" version = "0.6.1" @@ -677,6 +722,36 @@ dependencies = [ "serde", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -694,6 +769,17 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -727,6 +813,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "asynchronous-codec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "ata_core" version = "0.1.0" @@ -760,6 +859,29 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" +dependencies = [ + "http 0.2.12", + "log", + "url", +] + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http 1.4.0", + "log", + "url", +] + [[package]] name = "attribute-derive" version = "0.10.5" @@ -806,7 +928,7 @@ dependencies = [ "axum-core 0.4.5", "bytes", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -840,7 +962,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -875,7 +997,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -894,7 +1016,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -905,6 +1027,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers 0.3.0", + "tokio", +] + [[package]] name = "base-x" version = "0.2.11" @@ -957,24 +1090,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bedrock_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "common", - "futures", - "humantime-serde", - "log", - "logos-blockchain-chain-broadcast-service", - "logos-blockchain-chain-service", - "logos-blockchain-common-http-client", - "logos-blockchain-core", - "reqwest", - "serde", - "tokio-retry", -] - [[package]] name = "bincode" version = "1.3.3" @@ -1101,7 +1216,7 @@ dependencies = [ "futures-util", "hex", "home", - "http", + "http 1.4.0", "http-body-util", "hyper", "hyper-named-pipe", @@ -1354,17 +1469,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "cfg_eval" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "chacha20" version = "0.10.0" @@ -1374,6 +1478,7 @@ dependencies = [ "cfg-if", "cipher 0.5.1", "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1959,7 +2064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -1973,6 +2078,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -2123,10 +2242,21 @@ dependencies = [ ] [[package]] -name = "docker-compose-types" -version = "0.22.0" +name = "dlopen2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edb75a85449fd9c34d9fb3376c6208ec4115d2ca43b965175a52d71349ecab8" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "docker-compose-types" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea51e75cfa9371c4d760270c3da13516d7206121d668c1fbdd6fd83d1782b0f" dependencies = [ "derive_builder", "indexmap 2.13.0", @@ -2177,6 +2307,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + [[package]] name = "duplicate" version = "2.0.1" @@ -2215,6 +2351,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ + "pkcs8", "serde", "signature", ] @@ -2316,6 +2453,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "enum-map" version = "2.7.3" @@ -2501,12 +2650,12 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ferroid" -version = "0.8.9" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" dependencies = [ "portable-atomic", - "rand 0.9.3", + "rand 0.10.1", "web-time", ] @@ -2583,15 +2732,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -2599,7 +2739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -2613,12 +2753,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -2655,6 +2789,16 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-bounded" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91f328e7fb845fc832912fb6a34f40cf6d1888c92f974d1893a54e97b5ff542e" +dependencies = [ + "futures-timer", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -2688,6 +2832,16 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -2699,6 +2853,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -2717,7 +2882,7 @@ version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ - "gloo-timers", + "gloo-timers 0.2.6", "send_wrapper 0.4.0", ] @@ -2821,6 +2986,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", "wasm-bindgen", @@ -2863,7 +3029,7 @@ dependencies = [ "futures-core", "futures-sink", "gloo-utils", - "http", + "http 1.4.0", "js-sys", "pin-project", "serde", @@ -2886,6 +3052,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -2927,7 +3105,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.4.0", "indexmap 2.13.0", "slab", "tokio", @@ -2975,6 +3153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", + "equivalent", "foldhash", ] @@ -2984,6 +3163,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "hashlink" version = "0.10.0" @@ -3013,6 +3201,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3040,6 +3234,59 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hickory-proto" +version = "0.25.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d00147af6310f4392a31680db52a3ed45a2e0f68eb18e8c3fe5537ecc96d9e2" +dependencies = [ + "async-recursion", + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.3", + "socket2 0.5.10", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762f69ebdbd4ddb2e975cd24690bf21fe6b2604039189c26acddbc427f12887" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.3", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3093,6 +3340,17 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -3110,7 +3368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -3121,7 +3379,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "pin-project-lite", ] @@ -3196,7 +3454,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http", + "http 1.4.0", "http-body", "httparse", "httpdate", @@ -3229,7 +3487,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http", + "http 1.4.0", "hyper", "hyper-util", "log", @@ -3254,22 +3512,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -3280,19 +3522,17 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.4.0", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2", - "system-configuration", + "socket2 0.6.3", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -3448,6 +3688,81 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "if-watch" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c02a5161c313f0cbdbadc511611893584a10a7b6153cb554bdf83ddce99ec2" +dependencies = [ + "async-io", + "core-foundation 0.9.4", + "fnv", + "futures", + "if-addrs", + "ipnet", + "log", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "rtnetlink", + "system-configuration 0.7.0", + "tokio", + "windows", +] + +[[package]] +name = "igd-next" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b0d7d4541def58a37bf8efc559683f21edce7c82f0d866c93ac21f7e098f93" +dependencies = [ + "async-trait", + "attohttpc 0.24.1", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.8.5", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc 0.30.1", + "bytes", + "futures", + "http 1.4.0", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.3", + "tokio", + "url", + "xmltree", +] + [[package]] name = "include_bytes_aligned" version = "0.1.4" @@ -3460,13 +3775,13 @@ version = "0.1.0" dependencies = [ "anyhow", "async-stream", - "bedrock_client", "borsh", "common", "futures", "humantime-serde", "log", "logos-blockchain-core", + "logos-blockchain-zone-sdk", "nssa", "nssa_core", "serde", @@ -3602,6 +3917,7 @@ dependencies = [ "indexer_ffi", "indexer_service", "indexer_service_rpc", + "jsonrpsee", "key_protocol", "log", "nssa", @@ -3635,6 +3951,19 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.3", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -3818,7 +4147,7 @@ dependencies = [ "futures-channel", "futures-util", "gloo-net", - "http", + "http 1.4.0", "jsonrpsee-core", "pin-project", "rustls", @@ -3843,7 +4172,7 @@ dependencies = [ "bytes", "futures-timer", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "jsonrpsee-types", @@ -3904,7 +4233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" dependencies = [ "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -3930,7 +4259,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" dependencies = [ - "http", + "http 1.4.0", "serde", "serde_json", "thiserror 2.0.18", @@ -3954,7 +4283,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" dependencies = [ - "http", + "http 1.4.0", "jsonrpsee-client-transport", "jsonrpsee-core", "jsonrpsee-types", @@ -3993,6 +4322,7 @@ dependencies = [ "aes-gcm", "anyhow", "base58", + "bincode", "bip39", "common", "hex", @@ -4309,18 +4639,401 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libp2p" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72dc443ddd0254cb49a794ed6b6728400ee446a0f7ab4a07d0209ee98de20e9" +dependencies = [ + "bytes", + "either", + "futures", + "futures-timer", + "getrandom 0.2.17", + "libp2p-allow-block-list", + "libp2p-autonat", + "libp2p-connection-limits", + "libp2p-core", + "libp2p-dns", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-mdns", + "libp2p-metrics", + "libp2p-quic", + "libp2p-swarm", + "libp2p-tcp", + "libp2p-upnp", + "multiaddr", + "pin-project", + "rw-stream-sink", + "thiserror 2.0.18", +] + +[[package]] +name = "libp2p-allow-block-list" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38944b7cb981cc93f2f0fb411ff82d0e983bd226fbcc8d559639a3a73236568b" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-autonat" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e297bfc6cabb70c6180707f8fa05661b77ecb9cb67e8e8e1c469301358fa21d0" +dependencies = [ + "async-trait", + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-request-response", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "rand_core 0.6.4", + "thiserror 2.0.18", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-connection-limits" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe9323175a17caa8a2ed4feaf8a548eeef5e0b72d03840a0eab4bcb0210ce1c" +dependencies = [ + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", +] + +[[package]] +name = "libp2p-core" +version = "0.43.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249128cd37a2199aff30a7675dffa51caf073b51aa612d2f544b19932b9aebca" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-identity", + "multiaddr", + "multihash", + "multistream-select", + "parking_lot", + "pin-project", + "quick-protobuf", + "rand 0.8.5", + "rw-stream-sink", + "thiserror 2.0.18", + "tracing", + "unsigned-varint 0.8.0", + "web-time", +] + +[[package]] +name = "libp2p-dns" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b780a1150214155b0ed1cdf09fbd2e1b0442604f9146a431d1b21d23eef7bd7" +dependencies = [ + "async-trait", + "futures", + "hickory-resolver", + "libp2p-core", + "libp2p-identity", + "parking_lot", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d558548fa3b5a8e9b66392f785921e363c57c05dcadfda4db0d41ae82d313e4a" +dependencies = [ + "async-channel", + "asynchronous-codec", + "base64 0.22.1", + "byteorder", + "bytes", + "either", + "fnv", + "futures", + "futures-timer", + "getrandom 0.2.17", + "hashlink 0.9.1", + "hex_fmt", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "prometheus-client", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "regex", + "serde", + "sha2", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-identify" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c06862544f02d05d62780ff590cc25a75f5c2b9df38ec7a370dcae8bb873cf" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "libp2p-identity" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3" dependencies = [ + "asn1_der", "bs58", + "ed25519-dalek", "hkdf", + "k256", "multihash", + "quick-protobuf", + "rand 0.8.5", + "serde", "sha2", "thiserror 2.0.18", "tracing", + "zeroize", +] + +[[package]] +name = "libp2p-kad" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bab0466a27ebe955bcbc27328fae5429c5b48c915fd6174931414149802ec23" +dependencies = [ + "asynchronous-codec", + "bytes", + "either", + "fnv", + "futures", + "futures-bounded", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "quick-protobuf", + "quick-protobuf-codec", + "rand 0.8.5", + "serde", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tracing", + "uint", + "web-time", +] + +[[package]] +name = "libp2p-mdns" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d0ba095e1175d797540e16b62e7576846b883cb5046d4159086837b36846cc" +dependencies = [ + "futures", + "hickory-proto", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "smallvec", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-metrics" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce58c64292e87af624fcb86465e7dd8342e46a388d71e8fec0ab37ee789630a" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-gossipsub", + "libp2p-identify", + "libp2p-identity", + "libp2p-kad", + "libp2p-swarm", + "pin-project", + "prometheus-client", + "web-time", +] + +[[package]] +name = "libp2p-quic" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41432a159b00424a0abaa2c80d786cddff81055ac24aa127e0cf375f7858d880" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libp2p-core", + "libp2p-identity", + "libp2p-tls", + "quinn", + "rand 0.8.5", + "ring", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-request-response" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548fe44a80ff275d400f1b26b090d441d83ef73efabbeb6415f4ce37e5aed865" +dependencies = [ + "async-trait", + "futures", + "futures-bounded", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "smallvec", + "tracing", +] + +[[package]] +name = "libp2p-stream" +version = "0.3.0-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826716f1ee125895f1fb44911413cba023485b552ff96c7a2159bd037ac619bb" +dependencies = [ + "futures", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm", + "rand 0.8.5", + "tracing", +] + +[[package]] +name = "libp2p-swarm" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803399b4b6f68adb85e63ab573ac568154b193e9a640f03e0f2890eabbcb37f8" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "libp2p-core", + "libp2p-identity", + "libp2p-swarm-derive", + "lru", + "multistream-select", + "once_cell", + "rand 0.8.5", + "smallvec", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "libp2p-swarm-derive" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206e0aa0ebe004d778d79fb0966aa0de996c19894e2c0605ba2f8524dd4443d8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "libp2p-tcp" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65346fb4d36035b23fec4e7be4c320436ba53537ce9b6be1d1db1f70c905cad0" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "libp2p-tls" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ff65a82e35375cbc31ebb99cacbbf28cb6c4fefe26bf13756ddcf708d40080" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core", + "libp2p-identity", + "rcgen", + "ring", + "rustls", + "rustls-webpki", + "thiserror 2.0.18", + "x509-parser", + "yasna", +] + +[[package]] +name = "libp2p-upnp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d457b9ecceb66e7199f049926fad447f1f17f040e8d29d690c086b4cab8ed14a" +dependencies = [ + "futures", + "futures-timer", + "igd-next 0.15.1", + "libp2p-core", + "libp2p-swarm", + "tokio", + "tracing", ] [[package]] @@ -4400,8 +5113,8 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos-blockchain-blend-crypto" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "blake2", "logos-blockchain-groth16", @@ -4414,8 +5127,8 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-message" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "blake2", "derivative", @@ -4437,8 +5150,8 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-proofs" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "ed25519-dalek", "generic-array 1.3.5", @@ -4456,8 +5169,8 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-broadcast-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "derivative", @@ -4472,11 +5185,12 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "bytes", + "derivative", "futures", "logos-blockchain-chain-broadcast-service", "logos-blockchain-core", @@ -4490,7 +5204,6 @@ dependencies = [ "logos-blockchain-time-service", "logos-blockchain-tracing", "logos-blockchain-utils", - "num-bigint 0.4.6", "overwatch", "serde", "serde_with", @@ -4503,8 +5216,8 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-prover" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "logos-blockchain-circuits-utils", "tempfile", @@ -4512,16 +5225,16 @@ dependencies = [ [[package]] name = "logos-blockchain-circuits-utils" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "dirs", ] [[package]] name = "logos-blockchain-common-http-client" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "futures", "hex", @@ -4540,8 +5253,8 @@ dependencies = [ [[package]] name = "logos-blockchain-core" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "ark-ff 0.4.2", "bincode", @@ -4554,6 +5267,7 @@ dependencies = [ "logos-blockchain-cryptarchia-engine", "logos-blockchain-groth16", "logos-blockchain-key-management-system-keys", + "logos-blockchain-mmr", "logos-blockchain-poc", "logos-blockchain-pol", "logos-blockchain-poseidon2", @@ -4562,6 +5276,7 @@ dependencies = [ "multiaddr", "nom 8.0.0", "num-bigint 0.4.6", + "rpds", "serde", "strum", "thiserror 1.0.69", @@ -4570,10 +5285,9 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-engine" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ - "cfg_eval", "logos-blockchain-pol", "logos-blockchain-utils", "serde", @@ -4586,11 +5300,13 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-sync" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "bytes", "futures", + "libp2p", + "libp2p-stream", "logos-blockchain-core", "logos-blockchain-cryptarchia-engine", "rand 0.8.5", @@ -4603,8 +5319,8 @@ dependencies = [ [[package]] name = "logos-blockchain-groth16" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "ark-bn254 0.4.0", "ark-ec 0.4.2", @@ -4621,8 +5337,8 @@ dependencies = [ [[package]] name = "logos-blockchain-http-api-common" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "axum 0.7.9", "logos-blockchain-core", @@ -4636,8 +5352,8 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-keys" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "bytes", @@ -4662,8 +5378,8 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-macros" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "proc-macro2", "quote", @@ -4672,8 +5388,8 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-operators" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "logos-blockchain-blend-proofs", @@ -4688,8 +5404,8 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "log", @@ -4705,8 +5421,8 @@ dependencies = [ [[package]] name = "logos-blockchain-ledger" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "derivative", "logos-blockchain-blend-crypto", @@ -4716,6 +5432,7 @@ dependencies = [ "logos-blockchain-cryptarchia-engine", "logos-blockchain-groth16", "logos-blockchain-key-management-system-keys", + "logos-blockchain-mmr", "logos-blockchain-pol", "logos-blockchain-utils", "logos-blockchain-utxotree", @@ -4728,17 +5445,61 @@ dependencies = [ "tracing", ] +[[package]] +name = "logos-blockchain-libp2p" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +dependencies = [ + "async-trait", + "backon", + "blake2", + "either", + "futures", + "hex", + "igd-next 0.16.2", + "libp2p", + "logos-blockchain-cryptarchia-sync", + "logos-blockchain-utils", + "multiaddr", + "natpmp", + "netdev", + "num_enum", + "rand 0.8.5", + "serde", + "serde_with", + "thiserror 1.0.69", + "tokio", + "tracing", + "zerocopy", +] + +[[package]] +name = "logos-blockchain-mmr" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +dependencies = [ + "ark-ff 0.4.2", + "logos-blockchain-groth16", + "logos-blockchain-poseidon2", + "rpds", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "logos-blockchain-network-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "futures", "logos-blockchain-core", "logos-blockchain-cryptarchia-sync", + "logos-blockchain-libp2p", "logos-blockchain-tracing", "overwatch", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "tokio", "tokio-stream", @@ -4747,8 +5508,8 @@ dependencies = [ [[package]] name = "logos-blockchain-poc" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4763,8 +5524,8 @@ dependencies = [ [[package]] name = "logos-blockchain-pol" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "astro-float", "logos-blockchain-circuits-prover", @@ -4782,8 +5543,8 @@ dependencies = [ [[package]] name = "logos-blockchain-poq" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4799,8 +5560,8 @@ dependencies = [ [[package]] name = "logos-blockchain-poseidon2" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", @@ -4810,8 +5571,8 @@ dependencies = [ [[package]] name = "logos-blockchain-services-utils" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "futures", @@ -4825,8 +5586,8 @@ dependencies = [ [[package]] name = "logos-blockchain-storage-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "bytes", @@ -4843,15 +5604,18 @@ dependencies = [ [[package]] name = "logos-blockchain-time-service" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "futures", "log", "logos-blockchain-cryptarchia-engine", "logos-blockchain-tracing", + "logos-blockchain-utils", "overwatch", + "serde", + "serde_with", "sntpc", "thiserror 2.0.18", "time", @@ -4862,8 +5626,8 @@ dependencies = [ [[package]] name = "logos-blockchain-tracing" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "opentelemetry", "opentelemetry-appender-tracing", @@ -4874,6 +5638,7 @@ dependencies = [ "rand 0.8.5", "serde", "tokio", + "tonic", "tracing", "tracing-appender", "tracing-gelf", @@ -4885,8 +5650,8 @@ dependencies = [ [[package]] name = "logos-blockchain-utils" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "async-trait", "blake2", @@ -4902,8 +5667,8 @@ dependencies = [ [[package]] name = "logos-blockchain-utxotree" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -4916,16 +5681,16 @@ dependencies = [ [[package]] name = "logos-blockchain-witness-generator" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "tempfile", ] [[package]] name = "logos-blockchain-zksign" -version = "0.2.1" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=1da154c74b911318fb853d37261f8a05ffe513b4#1da154c74b911318fb853d37261f8a05ffe513b4" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" dependencies = [ "logos-blockchain-circuits-prover", "logos-blockchain-circuits-utils", @@ -4939,6 +5704,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "logos-blockchain-zone-sdk" +version = "0.1.2" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=ee281a447d95a951752461ee0a6e88eb4a0f17cf#ee281a447d95a951752461ee0a6e88eb4a0f17cf" +dependencies = [ + "async-trait", + "futures", + "logos-blockchain-common-http-client", + "logos-blockchain-core", + "logos-blockchain-groth16", + "logos-blockchain-key-management-system-service", + "rand 0.8.5", + "reqwest", + "rpds", + "serde", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "loki-api" version = "0.1.3" @@ -4949,6 +5734,15 @@ dependencies = [ "prost-types 0.13.5", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -5153,7 +5947,7 @@ dependencies = [ "bitflags 2.11.0", "block", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", @@ -5202,6 +5996,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "multer" version = "3.1.0" @@ -5211,7 +6022,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.4.0", "httparse", "memchr", "mime", @@ -5234,7 +6045,8 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint", + "unsigned-varint 0.8.0", + "url", ] [[package]] @@ -5256,24 +6068,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" dependencies = [ "no_std_io2", - "unsigned-varint", + "serde", + "unsigned-varint 0.8.0", ] [[package]] -name = "native-tls" -version = "0.2.18" +name = "multistream-select" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "ea0df8e5eec2298a62b326ee4f0d7fe1a6b90a09dfcf9df37b38f947a8c42f19" dependencies = [ - "libc", + "bytes", + "futures", "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", + "pin-project", + "smallvec", + "unsigned-varint 0.7.2", +] + +[[package]] +name = "natpmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77366fa8ce34e2e1322dd97da65f11a62f451bd3daae8be6993c00800f61dd07" +dependencies = [ + "async-trait", + "cc", + "netdev", + "tokio", ] [[package]] @@ -5292,6 +6114,108 @@ dependencies = [ "rayon", ] +[[package]] +name = "netdev" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f901362e84cd407be6f8cd9d3a46bccf09136b095792785401ea7d283c79b91d" +dependencies = [ + "dlopen2", + "ipnet", + "libc", + "netlink-packet-core 0.7.0", + "netlink-packet-route 0.17.1", + "netlink-sys", + "once_cell", + "system-configuration 0.6.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +dependencies = [ + "anyhow", + "byteorder", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "byteorder", + "libc", + "netlink-packet-core 0.7.0", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags 2.11.0", + "libc", + "log", + "netlink-packet-core 0.8.1", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror 1.0.69", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core 0.8.1", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + [[package]] name = "next_tuple" version = "0.1.0" @@ -5316,6 +6240,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.8.1" @@ -5399,7 +6335,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5580,6 +6516,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -5598,50 +6543,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "opentelemetry" version = "0.31.0" @@ -5675,7 +6582,7 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http", + "http 1.4.0", "opentelemetry", "reqwest", ] @@ -5686,7 +6593,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f" dependencies = [ - "http", + "http 1.4.0", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", @@ -5850,6 +6757,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -5930,6 +6847,20 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "polyval" version = "0.6.2" @@ -6104,6 +7035,29 @@ dependencies = [ "token_program", ] +[[package]] +name = "prometheus-client" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proptest" version = "1.10.0" @@ -6196,6 +7150,28 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + +[[package]] +name = "quick-protobuf-codec" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0580ab32b169745d7a39db2ba969226ca16738931be152a3209b409de2474" +dependencies = [ + "asynchronous-codec", + "bytes", + "quick-protobuf", + "thiserror 1.0.69", + "unsigned-varint 0.8.0", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6204,12 +7180,13 @@ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", + "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -6246,7 +7223,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.3", "tracing", "windows-sys 0.59.0", ] @@ -6321,6 +7298,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -6359,6 +7347,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -6394,6 +7388,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "reactive_graph" version = "0.2.13" @@ -6534,22 +7541,18 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -6560,7 +7563,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -6574,6 +7576,12 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "rfc6979" version = "0.4.0" @@ -7032,6 +8040,24 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core 0.8.1", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + [[package]] name = "ruint" version = "1.17.2" @@ -7075,6 +8101,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "1.1.4" @@ -7178,6 +8213,17 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "rw-stream-sink" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c9026ff5d2f23da5e45bbc283f156383001bfb09c4e44256d02c1a685fe9a1" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + [[package]] name = "ryu" version = "1.0.23" @@ -7341,17 +8387,16 @@ name = "sequencer_core" version = "0.1.0" dependencies = [ "anyhow", - "bedrock_client", "borsh", "bytesize", "chrono", "common", "futures", "humantime-serde", - "jsonrpsee", "log", "logos-blockchain-core", "logos-blockchain-key-management-system-service", + "logos-blockchain-zone-sdk", "mempool", "nssa", "nssa_core", @@ -7377,7 +8422,6 @@ dependencies = [ "common", "env_logger", "futures", - "indexer_service_rpc", "jsonrpsee", "log", "mempool", @@ -7608,7 +8652,7 @@ dependencies = [ "const_format", "futures", "gloo-net", - "http", + "http 1.4.0", "http-body-util", "hyper", "inventory", @@ -7760,6 +8804,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -7779,7 +8833,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http", + "http 1.4.0", "httparse", "log", "rand 0.8.5", @@ -7949,6 +9003,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -8002,6 +9067,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -8073,9 +9144,9 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd36b06a2a6c0c3c81a83be1ab05fe86460d054d4d51bf513bc56b3e15bdc22" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" dependencies = [ "astral-tokio-tar", "async-trait", @@ -8087,7 +9158,7 @@ dependencies = [ "etcetera", "ferroid", "futures", - "http", + "http 1.4.0", "itertools 0.14.0", "log", "memchr", @@ -8267,7 +9338,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -8283,27 +9354,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-retry" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" -dependencies = [ - "pin-project", - "rand 0.8.5", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -8464,7 +9514,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -8472,7 +9522,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2", + "socket2 0.6.3", "sync_wrapper", "tokio", "tokio-stream", @@ -8522,7 +9572,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "http-range-header", @@ -8730,7 +9780,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.3", @@ -8811,6 +9861,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "uint" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "unarray" version = "0.1.4" @@ -8878,6 +9940,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + [[package]] name = "unsigned-varint" version = "0.8.0" @@ -8912,7 +9980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" dependencies = [ "base64 0.22.1", - "http", + "http 1.4.0", "httparse", "log", ] @@ -9279,6 +10347,12 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -9310,6 +10384,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -9323,6 +10418,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -9351,6 +10457,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-registry" version = "0.6.1" @@ -9447,6 +10563,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -9661,6 +10786,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -9671,6 +10813,21 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -9685,7 +10842,7 @@ checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", - "hashlink", + "hashlink 0.10.0", ] [[package]] @@ -9694,6 +10851,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 96b06460..4fcaf9f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ members = [ "examples/program_deployment", "examples/program_deployment/methods", "examples/program_deployment/methods/guest", - "bedrock_client", "testnet_initial_state", "indexer_ffi", ] @@ -67,7 +66,6 @@ amm_program = { path = "programs/amm" } ata_core = { path = "programs/associated_token_account/core" } ata_program = { path = "programs/associated_token_account" } test_program_methods = { path = "test_program_methods" } -bedrock_client = { path = "bedrock_client" } testnet_initial_state = { path = "testnet_initial_state" } tokio = { version = "1.50", features = [ @@ -122,11 +120,12 @@ tokio-retry = "0.3.0" schemars = "1.2" async-stream = "0.3.6" -logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } -logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } -logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } -logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } -logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "1da154c74b911318fb853d37261f8a05ffe513b4" } +logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } +logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } +logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } +logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } +logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } +logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" } rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 7a3ba9a9..18a7ff4b 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/associated_token_account.bin b/artifacts/program_methods/associated_token_account.bin index ebd5e406..ffd4931f 100644 Binary files a/artifacts/program_methods/associated_token_account.bin and b/artifacts/program_methods/associated_token_account.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index b8fc2da1..cb1b3ec5 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 84c4de29..b57e01f4 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 820bd198..428c6666 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index a9a80ec5..884bced6 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index ca97005f..9f4c8902 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index 496d7a21..4b3e8e42 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index d4f867f9..c72cc7db 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index f3131490..2d47f454 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 98238357..c02c7869 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index b51b516d..be4a54b2 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index e31d4f92..74c5451b 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 05778e6d..36465f3b 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/clock_chain_caller.bin b/artifacts/test_program_methods/clock_chain_caller.bin index 63f1896e..e6670230 100644 Binary files a/artifacts/test_program_methods/clock_chain_caller.bin and b/artifacts/test_program_methods/clock_chain_caller.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index c431ea88..84fe82f5 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index 3545111d..6896dca8 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index e4859678..2b3704b3 100644 Binary files a/artifacts/test_program_methods/flash_swap_callback.bin and b/artifacts/test_program_methods/flash_swap_callback.bin differ diff --git a/artifacts/test_program_methods/flash_swap_initiator.bin b/artifacts/test_program_methods/flash_swap_initiator.bin index 3c843722..d703e4a9 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index f8589cea..14f9ed3b 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/malicious_caller_program_id.bin b/artifacts/test_program_methods/malicious_caller_program_id.bin index b3bdb233..339cde2f 100644 Binary files a/artifacts/test_program_methods/malicious_caller_program_id.bin and b/artifacts/test_program_methods/malicious_caller_program_id.bin differ diff --git a/artifacts/test_program_methods/malicious_self_program_id.bin b/artifacts/test_program_methods/malicious_self_program_id.bin index bade7a1f..c0702c64 100644 Binary files a/artifacts/test_program_methods/malicious_self_program_id.bin and b/artifacts/test_program_methods/malicious_self_program_id.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 9920c6c4..ebbeb4a9 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index d765178b..712aaec4 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 635b15cd..becf77ce 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 77b71103..2982604f 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index f6b2a34d..ce407564 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/pda_claimer.bin b/artifacts/test_program_methods/pda_claimer.bin index 0d902873..62b43ef1 100644 Binary files a/artifacts/test_program_methods/pda_claimer.bin and b/artifacts/test_program_methods/pda_claimer.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 9fe7b9e4..49567b4a 100644 Binary files a/artifacts/test_program_methods/pinata_cooldown.bin and b/artifacts/test_program_methods/pinata_cooldown.bin differ diff --git a/artifacts/test_program_methods/private_pda_claimer.bin b/artifacts/test_program_methods/private_pda_claimer.bin new file mode 100644 index 00000000..5a64c66d Binary files /dev/null and b/artifacts/test_program_methods/private_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/private_pda_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index d304fa39..3e9cf0b9 100644 Binary files a/artifacts/test_program_methods/private_pda_delegator.bin and b/artifacts/test_program_methods/private_pda_delegator.bin differ diff --git a/artifacts/test_program_methods/private_pda_spender.bin b/artifacts/test_program_methods/private_pda_spender.bin new file mode 100644 index 00000000..97df6feb Binary files /dev/null and b/artifacts/test_program_methods/private_pda_spender.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 61527451..e6c918a6 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index bd13572b..f47e96bd 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/artifacts/test_program_methods/time_locked_transfer.bin b/artifacts/test_program_methods/time_locked_transfer.bin index 0c5d39ef..37aa4c4c 100644 Binary files a/artifacts/test_program_methods/time_locked_transfer.bin and b/artifacts/test_program_methods/time_locked_transfer.bin differ diff --git a/artifacts/test_program_methods/two_pda_claimer.bin b/artifacts/test_program_methods/two_pda_claimer.bin index 34476982..34896705 100644 Binary files a/artifacts/test_program_methods/two_pda_claimer.bin and b/artifacts/test_program_methods/two_pda_claimer.bin differ diff --git a/artifacts/test_program_methods/validity_window.bin b/artifacts/test_program_methods/validity_window.bin index f8f3c615..55745ae2 100644 Binary files a/artifacts/test_program_methods/validity_window.bin and b/artifacts/test_program_methods/validity_window.bin differ diff --git a/artifacts/test_program_methods/validity_window_chain_caller.bin b/artifacts/test_program_methods/validity_window_chain_caller.bin index b29561f1..17a82331 100644 Binary files a/artifacts/test_program_methods/validity_window_chain_caller.bin and b/artifacts/test_program_methods/validity_window_chain_caller.bin differ diff --git a/bedrock/deployment-settings.yaml b/bedrock/deployment-settings.yaml index d0c05e24..7ef63f03 100644 --- a/bedrock/deployment-settings.yaml +++ b/bedrock/deployment-settings.yaml @@ -39,42 +39,42 @@ cryptarchia: threshold: 1 timestamp: 0 gossipsub_protocol: /integration/logos-blockchain/cryptarchia/proto/1.0.0 - genesis_state: - mantle_tx: - ops: + genesis_block: + header: + version: Bedrock + parent_block: '0000000000000000000000000000000000000000000000000000000000000000' + slot: 0 + block_root: b5f8787ac23674822414c70eea15d842da38f2e806ede1a73cf7b5cf0277da07 + proof_of_leadership: + proof: '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + entropy_contribution: '0000000000000000000000000000000000000000000000000000000000000000' + leader_key: '0000000000000000000000000000000000000000000000000000000000000000' + voucher_cm: '0000000000000000000000000000000000000000000000000000000000000000' + signature: '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + transactions: + - mantle_tx: + ops: - opcode: 0 payload: - inputs: [ ] + inputs: [] outputs: - - value: 1 - pk: d204000000000000000000000000000000000000000000000000000000000000 - - value: 100 - pk: 2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26 + - value: 1 + pk: d204000000000000000000000000000000000000000000000000000000000000 + - value: 100 + pk: '2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26' + - value: 1 + pk: ed266e6e887b9b97059dc1aa1b7b2e19b934291753c6336a163fe4ebaa28e717 - opcode: 17 payload: - channel_id: "0000000000000000000000000000000000000000000000000000000000000000" - inscription: [ 103, 101, 110, 101, 115, 105, 115 ] # "genesis" in bytes - parent: "0000000000000000000000000000000000000000000000000000000000000000" - signer: "0000000000000000000000000000000000000000000000000000000000000000" - execution_gas_price: 0 - storage_gas_price: 0 - ops_proofs: - - !ZkSig - pi_a: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] - pi_b: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] - pi_c: [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ] - - NoProof + channel_id: '0000000000000000000000000000000000000000000000000000000000000000' + inscription: '67656e65736973' + parent: '0000000000000000000000000000000000000000000000000000000000000000' + signer: '0000000000000000000000000000000000000000000000000000000000000000' + execution_gas_price: 0 + storage_gas_price: 0 + ops_proofs: + - !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + - !Ed25519Sig '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' time: slot_duration: '1.0' chain_start_time: PLACEHOLDER_CHAIN_START_TIME diff --git a/bedrock/docker-compose.yml b/bedrock/docker-compose.yml index 73795666..e16e505b 100644 --- a/bedrock/docker-compose.yml +++ b/bedrock/docker-compose.yml @@ -1,7 +1,7 @@ services: logos-blockchain-node-0: - image: ghcr.io/logos-blockchain/logos-blockchain@sha256:c5243681b353278cabb562a176f0a5cfbefc2056f18cebc47fe0e3720c29fb12 + image: ghcr.io/logos-blockchain/logos-blockchain@sha256:9f1829dea335c56f6ff68ae37ea872ed5313b96b69e8ffe143c02b7217de85fc ports: - "${PORT:-8080}:18080/tcp" volumes: diff --git a/bedrock_client/Cargo.toml b/bedrock_client/Cargo.toml deleted file mode 100644 index 2137cb74..00000000 --- a/bedrock_client/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "bedrock_client" -version = "0.1.0" -edition = "2024" -license = { workspace = true } - -[lints] -workspace = true - -[dependencies] -common.workspace = true - -reqwest.workspace = true -anyhow.workspace = true -tokio-retry.workspace = true -futures.workspace = true -log.workspace = true -serde.workspace = true -humantime-serde.workspace = true -logos-blockchain-common-http-client.workspace = true -logos-blockchain-core.workspace = true -logos-blockchain-chain-broadcast-service.workspace = true -logos-blockchain-chain-service.workspace = true diff --git a/bedrock_client/src/lib.rs b/bedrock_client/src/lib.rs deleted file mode 100644 index 4e9bfffd..00000000 --- a/bedrock_client/src/lib.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::time::Duration; - -use anyhow::{Context as _, Result}; -use common::config::BasicAuth; -use futures::{Stream, TryFutureExt as _}; -#[expect(clippy::single_component_path_imports, reason = "Satisfy machete")] -use humantime_serde; -use log::{info, warn}; -pub use logos_blockchain_chain_broadcast_service::BlockInfo; -use logos_blockchain_chain_service::CryptarchiaInfo; -pub use logos_blockchain_common_http_client::{CommonHttpClient, Error}; -pub use logos_blockchain_core::{block::Block, header::HeaderId, mantle::SignedMantleTx}; -use reqwest::{Client, Url}; -use serde::{Deserialize, Serialize}; -use tokio_retry::Retry; - -/// Fibonacci backoff retry strategy configuration. -#[derive(Debug, Copy, Clone, Serialize, Deserialize)] -pub struct BackoffConfig { - #[serde(with = "humantime_serde")] - pub start_delay: Duration, - pub max_retries: usize, -} - -impl Default for BackoffConfig { - fn default() -> Self { - Self { - start_delay: Duration::from_millis(100), - max_retries: 5, - } - } -} - -/// Simple wrapper -/// maybe extend in the future for our purposes -/// `Clone` is cheap because `CommonHttpClient` is internally reference counted (`Arc`). -#[derive(Clone)] -pub struct BedrockClient { - http_client: CommonHttpClient, - node_url: Url, - backoff: BackoffConfig, -} - -impl BedrockClient { - pub fn new(backoff: BackoffConfig, node_url: Url, auth: Option) -> Result { - info!("Creating Bedrock client with node URL {node_url}"); - let client = Client::builder() - //Add more fields if needed - .timeout(std::time::Duration::from_mins(1)) - .build() - .context("Failed to build HTTP client")?; - - let auth = auth.map(|a| { - logos_blockchain_common_http_client::BasicAuthCredentials::new(a.username, a.password) - }); - - let http_client = CommonHttpClient::new_with_client(client, auth); - Ok(Self { - http_client, - node_url, - backoff, - }) - } - - pub async fn post_transaction(&self, tx: SignedMantleTx) -> Result, Error> { - Retry::spawn(self.backoff_strategy(), || async { - match self - .http_client - .post_transaction(self.node_url.clone(), tx.clone()) - .await - { - Ok(()) => Ok(Ok(())), - Err(err) => match err { - // Retry arm. - // Retrying only reqwest errors: mainly connected to http. - Error::Request(_) => Err(err), - // Returning non-retryable error - Error::Server(_) | Error::Client(_) | Error::Url(_) => Ok(Err(err)), - }, - } - }) - .await - } - - pub async fn get_lib_stream(&self) -> Result, Error> { - self.http_client.get_lib_stream(self.node_url.clone()).await - } - - pub async fn get_block_by_id( - &self, - header_id: HeaderId, - ) -> Result>, Error> { - Retry::spawn(self.backoff_strategy(), || { - self.http_client - .get_block_by_id(self.node_url.clone(), header_id) - .inspect_err(|err| warn!("Block fetching failed with error: {err:#}")) - }) - .await - } - - pub async fn get_consensus_info(&self) -> Result { - Retry::spawn(self.backoff_strategy(), || { - self.http_client - .consensus_info(self.node_url.clone()) - .inspect_err(|err| warn!("Block fetching failed with error: {err:#}")) - }) - .await - } - - fn backoff_strategy(&self) -> impl Iterator { - let start_delay_millis = self - .backoff - .start_delay - .as_millis() - .try_into() - .expect("Start delay must be less than u64::MAX milliseconds"); - - tokio_retry::strategy::FibonacciBackoff::from_millis(start_delay_millis) - .take(self.backoff.max_retries) - } -} diff --git a/configs/docker-all-in-one/indexer_config.json b/configs/docker-all-in-one/indexer_config.json index c2b07e3e..ca99a90c 100644 --- a/configs/docker-all-in-one/indexer_config.json +++ b/configs/docker-all-in-one/indexer_config.json @@ -1,12 +1,8 @@ { "home": "./indexer/service", "consensus_info_polling_interval": "1s", - "bedrock_client_config": { - "addr": "http://logos-blockchain-node-0:18080", - "backoff": { - "start_delay": "100ms", - "max_retries": 5 - } + "bedrock_config": { + "addr": "http://logos-blockchain-node-0:18080" }, "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", "initial_accounts": [ diff --git a/indexer/core/Cargo.toml b/indexer/core/Cargo.toml index 33fe2d9d..d609f5cb 100644 --- a/indexer/core/Cargo.toml +++ b/indexer/core/Cargo.toml @@ -9,7 +9,7 @@ workspace = true [dependencies] common.workspace = true -bedrock_client.workspace = true +logos-blockchain-zone-sdk.workspace = true nssa.workspace = true nssa_core.workspace = true storage.workspace = true @@ -19,13 +19,13 @@ anyhow.workspace = true log.workspace = true serde.workspace = true humantime-serde.workspace = true -tokio.workspace = true borsh.workspace = true futures.workspace = true url.workspace = true logos-blockchain-core.workspace = true serde_json.workspace = true async-stream.workspace = true +tokio.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/indexer/core/src/block_store.rs b/indexer/core/src/block_store.rs index cff07b0f..71ddfd82 100644 --- a/indexer/core/src/block_store.rs +++ b/indexer/core/src/block_store.rs @@ -1,11 +1,12 @@ use std::{path::Path, sync::Arc}; -use anyhow::Result; -use bedrock_client::HeaderId; +use anyhow::{Context as _, Result}; use common::{ block::{BedrockStatus, Block}, transaction::{NSSATransaction, clock_invocation}, }; +use logos_blockchain_core::{header::HeaderId, mantle::ops::channel::MsgId}; +use logos_blockchain_zone_sdk::Slot; use nssa::{Account, AccountId, V03State}; use nssa_core::BlockId; use storage::indexer::RocksDBIO; @@ -103,6 +104,22 @@ impl IndexerStore { Ok(self.dbio.calculate_state_for_id(block_id)?) } + pub fn get_zone_cursor(&self) -> Result> { + let Some(bytes) = self.dbio.get_zone_sdk_indexer_cursor_bytes()? else { + return Ok(None); + }; + let cursor: (MsgId, Slot) = serde_json::from_slice(&bytes) + .context("Failed to deserialize stored zone-sdk indexer cursor")?; + Ok(Some(cursor)) + } + + pub fn set_zone_cursor(&self, cursor: &(MsgId, Slot)) -> Result<()> { + let bytes = + serde_json::to_vec(cursor).context("Failed to serialize zone-sdk indexer cursor")?; + self.dbio.put_zone_sdk_indexer_cursor_bytes(&bytes)?; + Ok(()) + } + /// Recalculation of final state directly from DB. /// /// Used for indexer healthcheck. diff --git a/indexer/core/src/config.rs b/indexer/core/src/config.rs index 291e54f5..40ac0870 100644 --- a/indexer/core/src/config.rs +++ b/indexer/core/src/config.rs @@ -6,7 +6,6 @@ use std::{ }; use anyhow::{Context as _, Result}; -pub use bedrock_client::BackoffConfig; use common::config::BasicAuth; use humantime_serde; pub use logos_blockchain_core::mantle::ops::channel::ChannelId; @@ -16,8 +15,6 @@ use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientConfig { - /// For individual RPC requests we use Fibonacci backoff retry strategy. - pub backoff: BackoffConfig, pub addr: Url, #[serde(default, skip_serializing_if = "Option::is_none")] pub auth: Option, @@ -31,7 +28,7 @@ pub struct IndexerConfig { pub signing_key: [u8; 32], #[serde(with = "humantime_serde")] pub consensus_info_polling_interval: Duration, - pub bedrock_client_config: ClientConfig, + pub bedrock_config: ClientConfig, pub channel_id: ChannelId, #[serde(skip_serializing_if = "Option::is_none")] pub initial_public_accounts: Option>, diff --git a/indexer/core/src/lib.rs b/indexer/core/src/lib.rs index 44f0dc19..3d57e540 100644 --- a/indexer/core/src/lib.rs +++ b/indexer/core/src/lib.rs @@ -1,15 +1,14 @@ -use std::collections::VecDeque; +use std::sync::Arc; use anyhow::Result; -use bedrock_client::{BedrockClient, HeaderId}; -use common::{ - HashType, PINATA_BASE58, - block::{Block, HashableBlockData}, -}; -use log::{debug, error, info}; -use logos_blockchain_core::mantle::{ - Op, SignedMantleTx, - ops::channel::{ChannelId, inscribe::InscriptionOp}, +use common::block::{Block, HashableBlockData}; +// ToDo: Remove after testnet +use common::{HashType, PINATA_BASE58}; +use futures::StreamExt as _; +use log::{error, info, warn}; +use logos_blockchain_core::header::HeaderId; +use logos_blockchain_zone_sdk::{ + CommonHttpClient, ZoneMessage, adapter::NodeHttpClient, indexer::ZoneIndexer, }; use nssa::V03State; use testnet_initial_state::initial_state_testnet; @@ -21,25 +20,11 @@ pub mod config; #[derive(Clone)] pub struct IndexerCore { - pub bedrock_client: BedrockClient, + pub zone_indexer: Arc>, pub config: IndexerConfig, pub store: IndexerStore, } -#[derive(Clone)] -/// This struct represents one L1 block data fetched from backfilling. -pub struct BackfillBlockData { - l2_blocks: Vec, - l1_header: HeaderId, -} - -#[derive(Clone)] -/// This struct represents data fetched fom backfilling in one iteration. -pub struct BackfillData { - block_data: VecDeque, - curr_fin_l1_lib_header: HeaderId, -} - impl IndexerCore { pub fn new(config: IndexerConfig) -> Result { let hashable_data = HashableBlockData { @@ -107,279 +92,88 @@ impl IndexerCore { let home = config.home.join("rocksdb"); + let basic_auth = config.bedrock_config.auth.clone().map(Into::into); + let node = NodeHttpClient::new( + CommonHttpClient::new(basic_auth), + config.bedrock_config.addr.clone(), + ); + let zone_indexer = ZoneIndexer::new(config.channel_id, node); + Ok(Self { - bedrock_client: BedrockClient::new( - config.bedrock_client_config.backoff, - config.bedrock_client_config.addr.clone(), - config.bedrock_client_config.auth.clone(), - )?, + zone_indexer: Arc::new(zone_indexer), config, store: IndexerStore::open_db_with_genesis(&home, &genesis_block, &state)?, }) } - pub fn subscribe_parse_block_stream(&self) -> impl futures::Stream> { + pub fn subscribe_parse_block_stream(&self) -> impl futures::Stream> + '_ { + let poll_interval = self.config.consensus_info_polling_interval; + let initial_cursor = self + .store + .get_zone_cursor() + .expect("Failed to load zone-sdk indexer cursor"); + async_stream::stream! { - info!("Searching for initial header"); + let mut cursor = initial_cursor; - let last_stored_l1_lib_header = self.store.last_observed_l1_lib_header()?; - - let mut prev_last_l1_lib_header = if let Some(last_l1_lib_header) = last_stored_l1_lib_header { - info!("Last l1 lib header found: {last_l1_lib_header}"); - last_l1_lib_header + if cursor.is_some() { + info!("Resuming indexer from cursor {cursor:?}"); } else { - info!("Last l1 lib header not found in DB"); - info!("Searching for the start of a channel"); - - let BackfillData { - block_data: start_buff, - curr_fin_l1_lib_header: last_l1_lib_header, - } = self.search_for_channel_start().await?; - - for BackfillBlockData { - l2_blocks: l2_block_vec, - l1_header, - } in start_buff { - let mut l2_blocks_parsed_ids: Vec<_> = l2_block_vec.iter().map(|block| block.header.block_id).collect(); - l2_blocks_parsed_ids.sort_unstable(); - info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); - - for l2_block in l2_block_vec { - // TODO: proper fix is to make the sequencer's genesis include a - // trailing `clock_invocation(0)` (and have the indexer's - // `open_db_with_genesis` not pre-apply state transitions) so the - // inscribed genesis can flow through `put_block` like any other - // block. For now we skip re-applying it. - // - // The channel-start (block_id == 1) is the sequencer's genesis - // inscription that we re-discover during initial search. The - // indexer already has its own locally-constructed genesis in - // the store from `open_db_with_genesis`, so re-applying the - // inscribed copy is both redundant and would fail the strict - // block validation in `put_block` (the inscribed genesis lacks - // the trailing clock invocation). - if l2_block.header.block_id != 1 { - self - .store - .put_block(l2_block.clone(), l1_header) - .await - .inspect_err(|err| error!("Failed to put block with err {err:?}"))?; - } - - yield Ok(l2_block); - } - } - - last_l1_lib_header - }; - - info!("Searching for initial header finished"); - - info!("Starting backfilling from {prev_last_l1_lib_header}"); + info!("Starting indexer from beginning of channel"); + } loop { - let BackfillData { - block_data: buff, - curr_fin_l1_lib_header, - } = self - .backfill_to_last_l1_lib_header_id(prev_last_l1_lib_header, &self.config.channel_id) - .await - .inspect_err(|err| error!("Failed to backfill to last l1 lib header id with err {err:#?}"))?; - - prev_last_l1_lib_header = curr_fin_l1_lib_header; - - for BackfillBlockData { - l2_blocks: l2_block_vec, - l1_header: header, - } in buff { - let mut l2_blocks_parsed_ids: Vec<_> = l2_block_vec.iter().map(|block| block.header.block_id).collect(); - l2_blocks_parsed_ids.sort_unstable(); - info!("Parsed {} L2 blocks with ids {:?}", l2_block_vec.len(), l2_blocks_parsed_ids); - - for l2_block in l2_block_vec { - self.store.put_block(l2_block.clone(), header).await?; - - yield Ok(l2_block); + let stream = match self.zone_indexer.next_messages(cursor).await { + Ok(s) => s, + Err(err) => { + error!("Failed to start zone-sdk next_messages stream: {err}"); + tokio::time::sleep(poll_interval).await; + continue; } - } - } - } - } - - async fn get_lib(&self) -> Result { - Ok(self.bedrock_client.get_consensus_info().await?.lib) - } - - async fn get_next_lib(&self, prev_lib: HeaderId) -> Result { - loop { - let next_lib = self.get_lib().await?; - if next_lib == prev_lib { - info!( - "Wait {:?} to not spam the node", - self.config.consensus_info_polling_interval - ); - tokio::time::sleep(self.config.consensus_info_polling_interval).await; - } else { - break Ok(next_lib); - } - } - } - - /// WARNING: depending on channel state, - /// may take indefinite amount of time. - pub async fn search_for_channel_start(&self) -> Result { - let mut curr_last_l1_lib_header = self.get_lib().await?; - let mut backfill_start = curr_last_l1_lib_header; - // ToDo: How to get root? - let mut backfill_limit = HeaderId::from([0; 32]); - // ToDo: Not scalable, initial buffer should be stored in DB to not run out of memory - // Don't want to complicate DB even more right now. - let mut block_buffer = VecDeque::new(); - - 'outer: loop { - let mut cycle_header = curr_last_l1_lib_header; - - loop { - let Some(cycle_block) = self.bedrock_client.get_block_by_id(cycle_header).await? - else { - // First run can reach root easily - // so here we are optimistic about L1 - // failing to get parent. - break; }; + let mut stream = std::pin::pin!(stream); - // It would be better to have id, but block does not have it, so slot will do. - info!( - "INITIAL SEARCH: Observed L1 block at slot {}", - cycle_block.header().slot().into_inner() - ); - debug!( - "INITIAL SEARCH: This block header is {}", - cycle_block.header().id() - ); - debug!( - "INITIAL SEARCH: This block parent is {}", - cycle_block.header().parent() - ); + while let Some((msg, slot)) = stream.next().await { + let zone_block = match msg { + ZoneMessage::Block(b) => b, + // Non-block messages don't carry a cursor position; the + // next ZoneBlock advances past them implicitly. + ZoneMessage::Deposit(_) | ZoneMessage::Withdraw(_) => continue, + }; - let (l2_block_vec, l1_header) = - parse_block_owned(&cycle_block, &self.config.channel_id); + let block: Block = match borsh::from_slice(&zone_block.data) { + Ok(b) => b, + Err(e) => { + error!("Failed to deserialize L2 block from zone-sdk: {e}"); + // Advance past the broken inscription so we don't + // re-process it on restart. + cursor = Some((zone_block.id, slot)); + if let Err(err) = self.store.set_zone_cursor(&(zone_block.id, slot)) { + warn!("Failed to persist indexer cursor: {err:#}"); + } + continue; + } + }; - info!("Parsed {} L2 blocks", l2_block_vec.len()); + info!("Indexed L2 block {}", block.header.block_id); - if !l2_block_vec.is_empty() { - block_buffer.push_front(BackfillBlockData { - l2_blocks: l2_block_vec.clone(), - l1_header, - }); - } - - if let Some(first_l2_block) = l2_block_vec.first() - && first_l2_block.header.block_id == 1 - { - info!("INITIAL_SEARCH: Found channel start"); - break 'outer; - } - - // Step back to parent - let parent = cycle_block.header().parent(); - - if parent == backfill_limit { - break; - } - - cycle_header = parent; - } - - info!("INITIAL_SEARCH: Reached backfill limit, refetching last l1 lib header"); - - block_buffer.clear(); - backfill_limit = backfill_start; - curr_last_l1_lib_header = self.get_next_lib(curr_last_l1_lib_header).await?; - backfill_start = curr_last_l1_lib_header; - } - - Ok(BackfillData { - block_data: block_buffer, - curr_fin_l1_lib_header: curr_last_l1_lib_header, - }) - } - - pub async fn backfill_to_last_l1_lib_header_id( - &self, - last_fin_l1_lib_header: HeaderId, - channel_id: &ChannelId, - ) -> Result { - let curr_fin_l1_lib_header = self.get_next_lib(last_fin_l1_lib_header).await?; - // ToDo: Not scalable, buffer should be stored in DB to not run out of memory - // Don't want to complicate DB even more right now. - let mut block_buffer = VecDeque::new(); - - let mut cycle_header = curr_fin_l1_lib_header; - loop { - let Some(cycle_block) = self.bedrock_client.get_block_by_id(cycle_header).await? else { - return Err(anyhow::anyhow!("Parent not found")); - }; - - if cycle_block.header().id() == last_fin_l1_lib_header { - break; - } - // Step back to parent - cycle_header = cycle_block.header().parent(); - - // It would be better to have id, but block does not have it, so slot will do. - info!( - "Observed L1 block at slot {}", - cycle_block.header().slot().into_inner() - ); - - let (l2_block_vec, l1_header) = parse_block_owned(&cycle_block, channel_id); - - info!("Parsed {} L2 blocks", l2_block_vec.len()); - - if !l2_block_vec.is_empty() { - block_buffer.push_front(BackfillBlockData { - l2_blocks: l2_block_vec, - l1_header, - }); - } - } - - Ok(BackfillData { - block_data: block_buffer, - curr_fin_l1_lib_header, - }) - } -} - -fn parse_block_owned( - l1_block: &bedrock_client::Block, - decoded_channel_id: &ChannelId, -) -> (Vec, HeaderId) { - ( - #[expect( - clippy::wildcard_enum_match_arm, - reason = "We are only interested in channel inscription ops, so it's fine to ignore the rest" - )] - l1_block - .transactions() - .flat_map(|tx| { - tx.mantle_tx.ops.iter().filter_map(|op| match op { - Op::ChannelInscribe(InscriptionOp { - channel_id, - inscription, - .. - }) if channel_id == decoded_channel_id => { - borsh::from_slice::(inscription) - .inspect_err(|err| { - error!("Failed to deserialize our inscription with err: {err:#?}"); - }) - .ok() + // TODO: Remove l1_header placeholder once storage layer + // no longer requires it. Zone-sdk handles L1 tracking internally. + let placeholder_l1_header = HeaderId::from([0_u8; 32]); + if let Err(err) = self.store.put_block(block.clone(), placeholder_l1_header).await { + error!("Failed to store block {}: {err:#}", block.header.block_id); } - _ => None, - }) - }) - .collect(), - l1_block.header().id(), - ) + + cursor = Some((zone_block.id, slot)); + if let Err(err) = self.store.set_zone_cursor(&(zone_block.id, slot)) { + warn!("Failed to persist indexer cursor: {err:#}"); + } + yield Ok(block); + } + + // Stream ended (caught up to LIB). Sleep then poll again. + tokio::time::sleep(poll_interval).await; + } + } + } } diff --git a/indexer/service/configs/indexer_config.json b/indexer/service/configs/indexer_config.json index e4dd8f93..558a3bfe 100644 --- a/indexer/service/configs/indexer_config.json +++ b/indexer/service/configs/indexer_config.json @@ -1,12 +1,8 @@ { "home": ".", "consensus_info_polling_interval": "1s", - "bedrock_client_config": { - "addr": "http://localhost:8080", - "backoff": { - "start_delay": "100ms", - "max_retries": 5 - } + "bedrock_config": { + "addr": "http://localhost:8080" }, "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", "initial_accounts": [ diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 53f0ee98..a1704cdf 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -19,8 +19,9 @@ indexer_service.workspace = true serde_json.workspace = true token_core.workspace = true ata_core.workspace = true -indexer_service_rpc.workspace = true +indexer_service_rpc = { workspace = true, features = ["client"] } sequencer_service_rpc = { workspace = true, features = ["client"] } +jsonrpsee = { workspace = true, features = ["ws-client"] } wallet-ffi.workspace = true indexer_ffi.workspace = true testnet_initial_state.workspace = true @@ -35,4 +36,4 @@ hex.workspace = true tempfile.workspace = true bytesize.workspace = true futures.workspace = true -testcontainers = { version = "0.27.0", features = ["docker-compose"] } +testcontainers = { version = "0.27.3", features = ["docker-compose"] } diff --git a/integration_tests/src/config.rs b/integration_tests/src/config.rs index faff1e79..7b3825de 100644 --- a/integration_tests/src/config.rs +++ b/integration_tests/src/config.rs @@ -2,7 +2,7 @@ use std::{net::SocketAddr, path::PathBuf, time::Duration}; use anyhow::{Context as _, Result}; use bytesize::ByteSize; -use indexer_service::{BackoffConfig, ChannelId, ClientConfig, IndexerConfig}; +use indexer_service::{ChannelId, ClientConfig, IndexerConfig}; use key_protocol::key_management::KeyChain; use nssa::{Account, AccountId, PrivateKey, PublicKey}; use nssa_core::{account::Data, program::DEFAULT_PROGRAM_ID}; @@ -164,35 +164,10 @@ impl std::fmt::Display for UrlProtocol { } } -pub fn indexer_config( - bedrock_addr: SocketAddr, - home: PathBuf, - initial_data: &InitialData, -) -> Result { - Ok(IndexerConfig { - home, - consensus_info_polling_interval: Duration::from_secs(1), - bedrock_client_config: ClientConfig { - addr: addr_to_url(UrlProtocol::Http, bedrock_addr) - .context("Failed to convert bedrock addr to URL")?, - auth: None, - backoff: BackoffConfig { - start_delay: Duration::from_millis(100), - max_retries: 10, - }, - }, - initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), - initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), - signing_key: [37; 32], - channel_id: bedrock_channel_id(), - }) -} - pub fn sequencer_config( partial: SequencerPartialConfig, home: PathBuf, bedrock_addr: SocketAddr, - indexer_addr: SocketAddr, initial_data: &InitialData, ) -> Result { let SequencerPartialConfig { @@ -215,17 +190,11 @@ pub fn sequencer_config( initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), signing_key: [37; 32], bedrock_config: BedrockConfig { - backoff: BackoffConfig { - start_delay: Duration::from_millis(100), - max_retries: 5, - }, channel_id: bedrock_channel_id(), node_url: addr_to_url(UrlProtocol::Http, bedrock_addr) .context("Failed to convert bedrock addr to URL")?, auth: None, }, - indexer_rpc_url: addr_to_url(UrlProtocol::Ws, indexer_addr) - .context("Failed to convert indexer addr to URL")?, }) } @@ -245,6 +214,26 @@ pub fn wallet_config( }) } +pub fn indexer_config( + bedrock_addr: SocketAddr, + home: PathBuf, + initial_data: &InitialData, +) -> Result { + Ok(IndexerConfig { + home, + consensus_info_polling_interval: Duration::from_secs(1), + bedrock_config: ClientConfig { + addr: addr_to_url(UrlProtocol::Http, bedrock_addr) + .context("Failed to convert bedrock addr to URL")?, + auth: None, + }, + initial_public_accounts: Some(initial_data.sequencer_initial_public_accounts()), + initial_private_accounts: Some(initial_data.sequencer_initial_private_accounts()), + signing_key: [37; 32], + channel_id: bedrock_channel_id(), + }) +} + pub fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result { // Convert 0.0.0.0 to 127.0.0.1 for client connections // When binding to port 0, the server binds to 0.0.0.0: diff --git a/integration_tests/src/indexer_client.rs b/integration_tests/src/indexer_client.rs new file mode 100644 index 00000000..5641d824 --- /dev/null +++ b/integration_tests/src/indexer_client.rs @@ -0,0 +1,34 @@ +//! Thin client wrapper for querying the indexer's JSON-RPC API in tests. +//! +//! The sequencer doesn't depend on the indexer at runtime — finalization comes +//! from zone-sdk events. This wrapper exists purely for test ergonomics so +//! integration tests can construct a single connection and call +//! `indexer_service_rpc::RpcClient` methods directly via `Deref`. + +use std::ops::Deref; + +use anyhow::{Context as _, Result}; +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use log::info; +use url::Url; + +pub struct IndexerClient(WsClient); + +impl IndexerClient { + pub async fn new(indexer_url: &Url) -> Result { + info!("Connecting to Indexer at {indexer_url}"); + let client = WsClientBuilder::default() + .build(indexer_url) + .await + .context("Failed to create websocket client")?; + Ok(Self(client)) + } +} + +impl Deref for IndexerClient { + type Target = WsClient; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 89715dfb..44c04560 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -9,16 +9,19 @@ use indexer_service::IndexerHandle; use log::{debug, error}; use nssa::{AccountId, PrivacyPreservingTransaction}; use nssa_core::Commitment; -use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _}; use sequencer_service::SequencerHandle; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use tempfile::TempDir; use testcontainers::compose::DockerCompose; use wallet::WalletCore; -use crate::setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet}; +use crate::{ + indexer_client::IndexerClient, + setup::{setup_bedrock_node, setup_indexer, setup_sequencer, setup_wallet}, +}; pub mod config; +pub mod indexer_client; pub mod setup; pub mod test_context_ffi; @@ -78,14 +81,10 @@ impl TestContext { .await .context("Failed to setup Indexer")?; - let (sequencer_handle, temp_sequencer_dir) = setup_sequencer( - sequencer_partial_config, - bedrock_addr, - indexer_handle.addr(), - &initial_data, - ) - .await - .context("Failed to setup Sequencer")?; + let (sequencer_handle, temp_sequencer_dir) = + setup_sequencer(sequencer_partial_config, bedrock_addr, &initial_data) + .await + .context("Failed to setup Sequencer")?; let (wallet, temp_wallet_dir, wallet_password) = setup_wallet(sequencer_handle.addr(), &initial_data) diff --git a/integration_tests/src/setup.rs b/integration_tests/src/setup.rs index 58b33c60..774c67e3 100644 --- a/integration_tests/src/setup.rs +++ b/integration_tests/src/setup.rs @@ -119,7 +119,6 @@ pub(crate) async fn setup_indexer( pub(crate) async fn setup_sequencer( partial: config::SequencerPartialConfig, bedrock_addr: SocketAddr, - indexer_addr: SocketAddr, initial_data: &config::InitialData, ) -> Result<(SequencerHandle, TempDir)> { let temp_sequencer_dir = @@ -134,7 +133,6 @@ pub(crate) async fn setup_sequencer( partial, temp_sequencer_dir.path().to_owned(), bedrock_addr, - indexer_addr, initial_data, ) .context("Failed to create Sequencer config")?; diff --git a/integration_tests/src/test_context_ffi.rs b/integration_tests/src/test_context_ffi.rs index 7d21aa28..b29ace7c 100644 --- a/integration_tests/src/test_context_ffi.rs +++ b/integration_tests/src/test_context_ffi.rs @@ -6,7 +6,6 @@ use indexer_ffi::IndexerServiceFFI; use indexer_service_rpc::RpcClient as _; use log::{debug, error}; use nssa::AccountId; -use sequencer_core::indexer_client::{IndexerClient, IndexerClientTrait as _}; use sequencer_service::SequencerHandle; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use tempfile::TempDir; @@ -15,6 +14,7 @@ use wallet::WalletCore; use crate::{ BEDROCK_SERVICE_WITH_OPEN_PORT, LOGGER, TestContextBuilder, config, + indexer_client::IndexerClient, setup::{setup_bedrock_node, setup_indexer_ffi, setup_sequencer, setup_wallet}, }; @@ -85,8 +85,6 @@ impl TestContextFFI { .block_on(setup_sequencer( sequencer_partial_config, bedrock_addr, - // SAFETY: addr is valid if indexer_ffi is valid. - unsafe { indexer_ffi.addr() }, initial_data, )) .context("Failed to setup Sequencer")?; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 41de30ed..df74daba 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -27,7 +27,7 @@ use nssa::{ public_transaction as putx, }; use nssa_core::{ - MembershipProof, NullifierPublicKey, + InputAccountIdentity, MembershipProof, NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -251,10 +251,19 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], - vec![(sender_npk, 0, sender_ss), (recipient_npk, 0, recipient_ss)], - vec![sender_nsk], - vec![Some(proof)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: sender_ss, + nsk: sender_nsk, + membership_proof: proof, + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_npk, + ssk: recipient_ss, + identifier: 0, + }, + ], &program.into(), ) .unwrap(); diff --git a/key_protocol/Cargo.toml b/key_protocol/Cargo.toml index 022f3ccd..72829ca8 100644 --- a/key_protocol/Cargo.toml +++ b/key_protocol/Cargo.toml @@ -26,3 +26,4 @@ itertools.workspace = true [dev-dependencies] base58.workspace = true +bincode.workspace = true diff --git a/key_protocol/src/key_management/group_key_holder.rs b/key_protocol/src/key_management/group_key_holder.rs new file mode 100644 index 00000000..1e7bf4e4 --- /dev/null +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -0,0 +1,505 @@ +use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; +use nssa_core::{ + SharedSecretKey, + encryption::{Scalar, shared_key_derivation::Secp256k1Point}, + program::PdaSeed, +}; +use rand::{RngCore as _, rngs::OsRng}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest as _, digest::FixedOutput as _}; + +use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey}; + +/// Public key used to seal a `GroupKeyHolder` for distribution to a recipient. +/// +/// Structurally identical to `ViewingPublicKey` (both are secp256k1 points), but given +/// a distinct alias to clarify intent: viewing keys encrypt account state, sealing keys +/// encrypt the GMS for off-chain distribution. +pub type SealingPublicKey = Secp256k1Point; + +/// Secret key used to unseal a `GroupKeyHolder` received from another member. +pub type SealingSecretKey = Scalar; + +/// Manages shared viewing keys for a group of controllers owning private PDAs. +/// +/// The Group Master Secret (GMS) is a 32-byte random value shared among controllers. +/// Each private PDA owned by the group gets a unique [`SecretSpendingKey`] derived from +/// the GMS by mixing the PDA seed into the SHA-256 input (see `secret_spending_key_for_pda`). +/// +/// # Distribution +/// +/// The GMS is a long-term secret and must never cross a trust boundary in raw form. +/// Controllers share it off-chain by sealing it under each recipient's [`SealingPublicKey`] +/// (see `seal_for` / `unseal`). Wallets persisting a `GroupKeyHolder` must encrypt it at +/// rest; the raw bytes are exposed only via [`GroupKeyHolder::dangerous_raw_gms`], which +/// is intended for the sealing path exclusively. +/// +/// # Logging safety +/// +/// `Debug` is implemented manually to redact the GMS; formatting this value with `{:?}` +/// will not leak the secret. Code that formats through `{:#?}` on containing types is +/// safe for the same reason. +#[derive(Serialize, Deserialize, Clone)] +pub struct GroupKeyHolder { + gms: [u8; 32], +} + +impl std::fmt::Debug for GroupKeyHolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GroupKeyHolder") + .field("gms", &"") + .finish() + } +} + +impl Default for GroupKeyHolder { + fn default() -> Self { + Self::new() + } +} + +impl GroupKeyHolder { + /// Create a new group with a fresh random GMS. + #[must_use] + pub fn new() -> Self { + let mut gms = [0_u8; 32]; + OsRng.fill_bytes(&mut gms); + Self { gms } + } + + /// Restore from an existing GMS (received via `unseal`). + #[must_use] + pub const fn from_gms(gms: [u8; 32]) -> Self { + Self { gms } + } + + /// Returns the raw 32-byte GMS. The name reflects intent: only the sealed-distribution + /// path (`seal_for`) and sealed-at-rest persistence should ever need the raw bytes. Do + /// not log the result, do not pass it across an untrusted channel. + #[must_use] + pub const fn dangerous_raw_gms(&self) -> &[u8; 32] { + &self.gms + } + + /// Derive a per-PDA [`SecretSpendingKey`] by mixing the seed into the SHA-256 input. + /// + /// Each distinct `pda_seed` produces a distinct SSK in the full 256-bit space, so + /// adversarial seed-grinding cannot collide two PDAs' derived keys under the same + /// group. Uses the codebase's 32-byte protocol-versioned domain-separation convention. + fn secret_spending_key_for_pda(&self, pda_seed: &PdaSeed) -> SecretSpendingKey { + const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeyDerivation/SSK"; + let mut hasher = sha2::Sha256::new(); + hasher.update(PREFIX); + hasher.update(self.gms); + hasher.update(pda_seed.as_ref()); + SecretSpendingKey(hasher.finalize_fixed().into()) + } + + /// Derive keys for a specific PDA. + /// + /// All controllers holding the same GMS independently derive the same keys for the + /// same PDA because the derivation is deterministic in (GMS, seed). + #[must_use] + pub fn derive_keys_for_pda(&self, pda_seed: &PdaSeed) -> PrivateKeyHolder { + self.secret_spending_key_for_pda(pda_seed) + .produce_private_key_holder(None) + } + + /// Encrypts this holder's GMS under the recipient's [`SealingPublicKey`]. + /// + /// Uses an ephemeral ECDH key exchange to derive a shared secret, then AES-256-GCM + /// to encrypt the payload. The returned bytes are + /// `ephemeral_pubkey (33) || nonce (12) || ciphertext+tag (48)` = 93 bytes. + /// + /// Each call generates a fresh ephemeral key, so two seals of the same holder produce + /// different ciphertexts. + #[must_use] + pub fn seal_for(&self, recipient_key: &SealingPublicKey) -> Vec { + let mut ephemeral_scalar: Scalar = [0_u8; 32]; + OsRng.fill_bytes(&mut ephemeral_scalar); + let ephemeral_pubkey = Secp256k1Point::from_scalar(ephemeral_scalar); + let shared = SharedSecretKey::new(&ephemeral_scalar, recipient_key); + let aes_key = Self::seal_kdf(&shared); + let cipher = Aes256Gcm::new(&aes_key.into()); + + let mut nonce_bytes = [0_u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = aes_gcm::Nonce::from(nonce_bytes); + + let ciphertext = cipher + .encrypt(&nonce, self.gms.as_ref()) + .expect("AES-GCM encryption should not fail with valid key/nonce"); + + let capacity = 33_usize + .checked_add(12) + .and_then(|n| n.checked_add(ciphertext.len())) + .expect("seal capacity overflow"); + let mut out = Vec::with_capacity(capacity); + out.extend_from_slice(&ephemeral_pubkey.0); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + out + } + + /// Decrypts a sealed `GroupKeyHolder` using the recipient's [`SealingSecretKey`]. + /// + /// Returns `Err` if the ciphertext is too short, the ECDH point is invalid, or the + /// AES-GCM authentication tag doesn't verify (wrong key or tampered data). + pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { + const HEADER_LEN: usize = 33 + 12; + const MIN_LEN: usize = HEADER_LEN + 16; + if sealed.len() < MIN_LEN { + return Err(SealError::TooShort); + } + // MIN_LEN (61) > HEADER_LEN (45), so all slicing below is in bounds. + let ephemeral_pubkey = Secp256k1Point(sealed[..33].to_vec()); + let nonce = aes_gcm::Nonce::from_slice(&sealed[33..HEADER_LEN]); + let ciphertext = &sealed[HEADER_LEN..]; + + let shared = SharedSecretKey::new(own_key, &ephemeral_pubkey); + let aes_key = Self::seal_kdf(&shared); + let cipher = Aes256Gcm::new(&aes_key.into()); + + let plaintext = cipher + .decrypt(nonce, ciphertext) + .map_err(|_err| SealError::DecryptionFailed)?; + + if plaintext.len() != 32 { + return Err(SealError::DecryptionFailed); + } + + let mut gms = [0_u8; 32]; + gms.copy_from_slice(&plaintext); + Ok(Self::from_gms(gms)) + } + + /// Derives an AES-256 key from the ECDH shared secret via SHA-256 with a domain prefix. + fn seal_kdf(shared: &SharedSecretKey) -> [u8; 32] { + const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeySeal/AES\x00\x00\x00\x00\x00\x00"; + let mut hasher = sha2::Sha256::new(); + hasher.update(PREFIX); + hasher.update(shared.0); + hasher.finalize_fixed().into() + } +} + +#[derive(Debug)] +pub enum SealError { + TooShort, + DecryptionFailed, +} + +#[cfg(test)] +mod tests { + use nssa_core::NullifierPublicKey; + + use super::*; + + /// Two holders from the same GMS derive identical keys for the same PDA seed. + #[test] + fn same_gms_same_seed_produces_same_keys() { + let gms = [42_u8; 32]; + let holder_a = GroupKeyHolder::from_gms(gms); + let holder_b = GroupKeyHolder::from_gms(gms); + let seed = PdaSeed::new([1; 32]); + + let keys_a = holder_a.derive_keys_for_pda(&seed); + let keys_b = holder_b.derive_keys_for_pda(&seed); + + assert_eq!( + keys_a.generate_nullifier_public_key().to_byte_array(), + keys_b.generate_nullifier_public_key().to_byte_array(), + ); + } + + /// Different PDA seeds produce different keys from the same GMS. + #[test] + fn same_gms_different_seed_produces_different_keys() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + let seed_a = PdaSeed::new([1; 32]); + let seed_b = PdaSeed::new([2; 32]); + + let npk_a = holder + .derive_keys_for_pda(&seed_a) + .generate_nullifier_public_key(); + let npk_b = holder + .derive_keys_for_pda(&seed_b) + .generate_nullifier_public_key(); + + assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array()); + } + + /// Different GMS produce different keys for the same PDA seed. + #[test] + fn different_gms_same_seed_produces_different_keys() { + let holder_a = GroupKeyHolder::from_gms([42_u8; 32]); + let holder_b = GroupKeyHolder::from_gms([99_u8; 32]); + let seed = PdaSeed::new([1; 32]); + + let npk_a = holder_a + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + let npk_b = holder_b + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + + assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array()); + } + + /// GMS round-trip: export and restore produces the same keys. + #[test] + fn gms_round_trip() { + let original = GroupKeyHolder::from_gms([7_u8; 32]); + let restored = GroupKeyHolder::from_gms(*original.dangerous_raw_gms()); + let seed = PdaSeed::new([1; 32]); + + let npk_original = original + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + let npk_restored = restored + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + + assert_eq!(npk_original.to_byte_array(), npk_restored.to_byte_array()); + } + + /// The derived `NullifierPublicKey` is non-zero (sanity check). + #[test] + fn derived_npk_is_non_zero() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + let seed = PdaSeed::new([1; 32]); + let npk = holder + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + + assert_ne!(npk, NullifierPublicKey([0; 32])); + } + + /// Pins the end-to-end derivation for a fixed (GMS, `ProgramId`, `PdaSeed`). Any change + /// to `secret_spending_key_for_pda`, the `PrivateKeyHolder` nsk/npk chain, or the + /// `AccountId::for_private_pda` formula breaks this test. Mirrors the pinned-value + /// pattern from `for_private_pda_matches_pinned_value` in `nssa_core`. + #[test] + fn pinned_end_to_end_derivation_for_private_pda() { + use nssa_core::{account::AccountId, program::ProgramId}; + + let gms = [42_u8; 32]; + let seed = PdaSeed::new([1; 32]); + let program_id: ProgramId = [9; 8]; + + let holder = GroupKeyHolder::from_gms(gms); + let npk = holder + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX); + + let expected_npk = NullifierPublicKey([ + 185, 161, 225, 224, 20, 156, 173, 0, 6, 173, 74, 136, 16, 88, 71, 154, 101, 160, 224, + 162, 247, 98, 183, 210, 118, 130, 143, 237, 20, 112, 111, 114, + ]); + + let expected_account_id = AccountId::new([ + 251, 228, 245, 3, 160, 134, 97, 69, 187, 157, 170, 192, 165, 216, 166, 79, 179, 187, + 125, 146, 36, 192, 232, 110, 198, 47, 24, 10, 223, 25, 108, 5, + ]); + + assert_eq!(npk, expected_npk); + assert_eq!(account_id, expected_account_id); + } + + /// Wallets persist `GroupKeyHolder` to disk and reload it on startup. This test pins + /// the serde round-trip: serialize, deserialize, and assert the derived keys for a + /// sample seed match on both sides. A silent encoding drift would corrupt every + /// group-owned account. + #[test] + fn gms_serde_round_trip_preserves_derivation() { + let original = GroupKeyHolder::from_gms([7_u8; 32]); + let encoded = bincode::serialize(&original).expect("serialize"); + let restored: GroupKeyHolder = bincode::deserialize(&encoded).expect("deserialize"); + + let seed = PdaSeed::new([1; 32]); + let npk_original = original + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + let npk_restored = restored + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + + assert_eq!(npk_original, npk_restored); + assert_eq!(original.dangerous_raw_gms(), restored.dangerous_raw_gms()); + } + + /// A `GroupKeyHolder` constructed from the same 32 bytes as a personal + /// `SecretSpendingKey` must not derive the same `NullifierPublicKey` as the personal + /// path, so a private PDA cannot be spent by a personal nullifier even under + /// adversarial key-material reuse. The safety rests on the group path's distinct + /// domain-separation prefix plus the seed mix-in (see `secret_spending_key_for_pda`). + #[test] + fn group_derivation_does_not_collide_with_personal_path_at_shared_bytes() { + let shared_bytes = [13_u8; 32]; + let seed = PdaSeed::new([5; 32]); + + let group_npk = GroupKeyHolder::from_gms(shared_bytes) + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(); + + let personal_npk = SecretSpendingKey(shared_bytes) + .produce_private_key_holder(None) + .generate_nullifier_public_key(); + + assert_ne!(group_npk, personal_npk); + } + + /// Seal then unseal recovers the same GMS and derived keys. + #[test] + fn seal_unseal_round_trip() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + + let recipient_ssk = SecretSpendingKey([7_u8; 32]); + let recipient_keys = recipient_ssk.produce_private_key_holder(None); + let recipient_vpk = recipient_keys.generate_viewing_public_key(); + let recipient_vsk = recipient_keys.viewing_secret_key; + + let sealed = holder.seal_for(&recipient_vpk); + let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); + + assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); + + let seed = PdaSeed::new([1; 32]); + assert_eq!( + holder + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(), + restored + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key(), + ); + } + + /// Unsealing with a different VSK fails with `DecryptionFailed`. + #[test] + fn unseal_wrong_vsk_fails() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + + let recipient_ssk = SecretSpendingKey([7_u8; 32]); + let recipient_vpk = recipient_ssk + .produce_private_key_holder(None) + .generate_viewing_public_key(); + + let wrong_ssk = SecretSpendingKey([99_u8; 32]); + let wrong_vsk = wrong_ssk + .produce_private_key_holder(None) + .viewing_secret_key; + + let sealed = holder.seal_for(&recipient_vpk); + let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); + assert!(matches!(result, Err(super::SealError::DecryptionFailed))); + } + + /// Tampered ciphertext fails authentication. + #[test] + fn unseal_tampered_ciphertext_fails() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + + let recipient_ssk = SecretSpendingKey([7_u8; 32]); + let recipient_keys = recipient_ssk.produce_private_key_holder(None); + let recipient_vpk = recipient_keys.generate_viewing_public_key(); + let recipient_vsk = recipient_keys.viewing_secret_key; + + let mut sealed = holder.seal_for(&recipient_vpk); + // Flip a byte in the ciphertext portion (after ephemeral_pubkey + nonce) + let last = sealed.len() - 1; + sealed[last] ^= 0xFF; + + let result = GroupKeyHolder::unseal(&sealed, &recipient_vsk); + assert!(matches!(result, Err(super::SealError::DecryptionFailed))); + } + + /// Two seals of the same holder produce different ciphertexts (ephemeral randomness). + #[test] + fn two_seals_produce_different_ciphertexts() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + + let recipient_ssk = SecretSpendingKey([7_u8; 32]); + let recipient_vpk = recipient_ssk + .produce_private_key_holder(None) + .generate_viewing_public_key(); + + let sealed_a = holder.seal_for(&recipient_vpk); + let sealed_b = holder.seal_for(&recipient_vpk); + assert_ne!(sealed_a, sealed_b); + } + + /// Sealed payload is too short. + #[test] + fn unseal_too_short_fails() { + let vsk: SealingSecretKey = [7_u8; 32]; + let result = GroupKeyHolder::unseal(&[0_u8; 10], &vsk); + assert!(matches!(result, Err(super::SealError::TooShort))); + } + + /// Degenerate GMS values (all-zeros, all-ones, single-bit) must still produce valid, + /// non-zero, pairwise-distinct npks. Rules out accidental "if gms == default { return + /// default }" style shortcuts in the derivation. + #[test] + fn degenerate_gms_produces_distinct_non_zero_keys() { + let seed = PdaSeed::new([1; 32]); + let degenerate = [[0_u8; 32], [0xFF_u8; 32], { + let mut v = [0_u8; 32]; + v[0] = 1; + v + }]; + + let npks: Vec = degenerate + .iter() + .map(|gms| { + GroupKeyHolder::from_gms(*gms) + .derive_keys_for_pda(&seed) + .generate_nullifier_public_key() + }) + .collect(); + + for npk in &npks { + assert_ne!(*npk, NullifierPublicKey([0; 32])); + } + for (i, a) in npks.iter().enumerate() { + for b in &npks[i + 1..] { + assert_ne!(a, b); + } + } + } + + /// Full lifecycle: create group, distribute GMS via seal/unseal, verify key agreement. + #[test] + fn group_pda_lifecycle() { + use nssa_core::account::AccountId; + + let alice_holder = GroupKeyHolder::new(); + let pda_seed = PdaSeed::new([42_u8; 32]); + let program_id: nssa_core::program::ProgramId = [1; 8]; + + // Derive Alice's keys + let alice_keys = alice_holder.derive_keys_for_pda(&pda_seed); + let alice_npk = alice_keys.generate_nullifier_public_key(); + + // Seal GMS for Bob using Bob's viewing key, Bob unseals + let bob_ssk = SecretSpendingKey([77_u8; 32]); + let bob_keys = bob_ssk.produce_private_key_holder(None); + let bob_vpk = bob_keys.generate_viewing_public_key(); + let bob_vsk = bob_keys.viewing_secret_key; + + let sealed = alice_holder.seal_for(&bob_vpk); + let bob_holder = + GroupKeyHolder::unseal(&sealed, &bob_vsk).expect("Bob should unseal the GMS"); + + // Key agreement: both derive identical NPK and AccountId + let bob_npk = bob_holder + .derive_keys_for_pda(&pda_seed) + .generate_nullifier_public_key(); + assert_eq!(alice_npk, bob_npk); + + let alice_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &alice_npk, 0); + let bob_account_id = AccountId::for_private_pda(&program_id, &pda_seed, &bob_npk, 0); + assert_eq!(alice_account_id, bob_account_id); + } +} diff --git a/key_protocol/src/key_management/mod.rs b/key_protocol/src/key_management/mod.rs index 065af364..aa5a1a75 100644 --- a/key_protocol/src/key_management/mod.rs +++ b/key_protocol/src/key_management/mod.rs @@ -6,6 +6,7 @@ use secret_holders::{PrivateKeyHolder, SecretSpendingKey, SeedHolder}; use serde::{Deserialize, Serialize}; pub mod ephemeral_key_holder; +pub mod group_key_holder; pub mod key_tree; pub mod secret_holders; diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index e6c08d90..4cda1c7a 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::key_management::{ KeyChain, + group_key_holder::GroupKeyHolder, key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, secret_holders::SeedHolder, }; @@ -30,6 +31,17 @@ pub struct NSSAUserData { pub public_key_tree: KeyTreePublic, /// Tree of private keys. pub private_key_tree: KeyTreePrivate, + /// Group key holders for private PDA groups, keyed by a human-readable label. + /// Defaults to empty for backward compatibility with wallets that predate group PDAs. + /// An older wallet binary that re-serializes this struct will drop the field. + #[serde(default)] + pub group_key_holders: BTreeMap, + /// Cached plaintext state of private PDA accounts, keyed by `AccountId`. + /// Updated after each private PDA transaction by decrypting the circuit output. + /// The sequencer only stores encrypted commitments, so this local cache is the + /// only source of plaintext state for private PDAs. + #[serde(default, alias = "group_pda_accounts")] + pub pda_accounts: BTreeMap, } impl NSSAUserData { @@ -88,6 +100,8 @@ impl NSSAUserData { default_user_private_accounts: default_accounts_key_chains, public_key_tree, private_key_tree, + group_key_holders: BTreeMap::new(), + pda_accounts: BTreeMap::new(), }) } @@ -193,6 +207,20 @@ impl NSSAUserData { .copied() .chain(self.private_key_tree.account_id_map.keys().copied()) } + + /// Returns the `GroupKeyHolder` for the given label, if it exists. + #[must_use] + pub fn group_key_holder(&self, label: &str) -> Option<&GroupKeyHolder> { + self.group_key_holders.get(label) + } + + /// Inserts or replaces a `GroupKeyHolder` under the given label. + /// + /// If a holder already exists under this label, it is silently replaced and the old + /// GMS is lost. Callers must ensure label uniqueness across groups. + pub fn insert_group_key_holder(&mut self, label: String, holder: GroupKeyHolder) { + self.group_key_holders.insert(label, holder); + } } impl Default for NSSAUserData { @@ -212,6 +240,26 @@ impl Default for NSSAUserData { mod tests { use super::*; + #[test] + fn group_key_holder_storage_round_trip() { + let mut user_data = NSSAUserData::default(); + assert!(user_data.group_key_holder("test-group").is_none()); + + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + user_data.insert_group_key_holder(String::from("test-group"), holder.clone()); + + let retrieved = user_data + .group_key_holder("test-group") + .expect("should exist"); + assert_eq!(retrieved.dangerous_raw_gms(), holder.dangerous_raw_gms()); + } + + #[test] + fn group_key_holders_default_empty() { + let user_data = NSSAUserData::default(); + assert!(user_data.group_key_holders.is_empty()); + } + #[test] fn new_account() { let mut user_data = NSSAUserData::default(); diff --git a/nssa/core/src/circuit_io.rs b/nssa/core/src/circuit_io.rs index c71003de..c993811d 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -12,23 +12,97 @@ use crate::{ pub struct PrivacyPreservingCircuitInput { /// Outputs of the program execution. pub program_outputs: Vec, - /// Visibility mask for accounts. - /// - /// - `0` - public account - /// - `1` - private account with authentication - /// - `2` - private account without authentication - /// - `3` - private PDA account - pub visibility_mask: Vec, - /// Public keys and identifiers of private accounts. - pub private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, - /// Nullifier secret keys for authorized private accounts. - pub private_account_nsks: Vec, - /// Membership proofs for private accounts. Can be [`None`] for uninitialized accounts. - pub private_account_membership_proofs: Vec>, + /// One entry per `pre_state`, in the same order as the program's `pre_states`. + /// Length must equal the number of `pre_states` derived from `program_outputs`. + /// The guest's `private_pda_npk_by_position` and `private_pda_bound_positions` + /// rely on this position alignment. + pub account_identities: Vec, /// Program ID. pub program_id: ProgramId, } +/// Per-account input to the privacy-preserving circuit. Each variant carries exactly the fields +/// the guest needs for that account's code path. +#[derive(Serialize, Deserialize, Clone)] +pub enum InputAccountIdentity { + /// Public account. The guest reads pre/post state from `program_outputs` and emits no + /// commitment, ciphertext, or nullifier. + Public, + /// Init of an authorized standalone private account: no membership proof. The `pre_state` + /// must be `Account::default()`. The `account_id` is derived as + /// `AccountId::from((&NullifierPublicKey::from(nsk), identifier))` and matched against + /// `pre_state.account_id`. + PrivateAuthorizedInit { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + identifier: Identifier, + }, + /// Update of an authorized standalone private account: existing on-chain commitment, with + /// membership proof. + PrivateAuthorizedUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + identifier: Identifier, + }, + /// Init of a standalone private account the caller does not own (e.g. a recipient who + /// doesn't yet exist on chain). No `nsk`, no membership proof. + PrivateUnauthorized { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + identifier: Identifier, + }, + /// Init of a private PDA, unauthorized. The npk-to-account_id binding is proven upstream + /// via `Claim::Pda(seed)` or a caller's `pda_seeds` match. The identifier diversifies the + /// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it + /// as the 4th input. + PrivatePdaInit { + npk: NullifierPublicKey, + ssk: SharedSecretKey, + identifier: Identifier, + }, + /// Update of an existing private PDA, authorized, with membership proof. `npk` is derived + /// from `nsk`. Authorization is established upstream by a caller `pda_seeds` match or a + /// previously-seen authorization in a chained call. + PrivatePdaUpdate { + ssk: SharedSecretKey, + nsk: NullifierSecretKey, + membership_proof: MembershipProof, + identifier: Identifier, + }, +} + +impl InputAccountIdentity { + #[must_use] + pub const fn is_public(&self) -> bool { + matches!(self, Self::Public) + } + + #[must_use] + pub const fn is_private_pda(&self) -> bool { + matches!( + self, + Self::PrivatePdaInit { .. } | Self::PrivatePdaUpdate { .. } + ) + } + + /// For private PDA variants, return the `(npk, identifier)` pair. `Init` carries both + /// directly; `Update` derives `npk` from `nsk`. For non-PDA variants returns `None`. + #[must_use] + pub fn npk_if_private_pda(&self) -> Option<(NullifierPublicKey, Identifier)> { + match self { + Self::PrivatePdaInit { npk, identifier, .. } => Some((*npk, *identifier)), + Self::PrivatePdaUpdate { nsk, identifier, .. } => { + Some((NullifierPublicKey::from(nsk), *identifier)) + } + Self::Public + | Self::PrivateAuthorizedInit { .. } + | Self::PrivateAuthorizedUpdate { .. } + | Self::PrivateUnauthorized { .. } => None, + } + } +} + #[derive(Serialize, Deserialize)] #[cfg_attr(any(feature = "host", test), derive(Debug, PartialEq, Eq))] pub struct PrivacyPreservingCircuitOutput { diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 7c585373..8392f18c 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -3,7 +3,9 @@ reason = "We prefer to group methods by functionality rather than by type for encoding" )] -pub use circuit_io::{PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput}; +pub use circuit_io::{ + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, +}; pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, compute_digest_for_path, diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index b9206492..c001b8d6 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -43,6 +43,12 @@ impl PdaSeed { } } +impl AsRef<[u8]> for PdaSeed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + impl AccountId { /// Derives an [`AccountId`] for a public PDA from the program ID and seed. #[must_use] diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index a0106344..6682a7de 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -2,8 +2,7 @@ use std::collections::{HashMap, VecDeque}; use borsh::{BorshDeserialize, BorshSerialize}; use nssa_core::{ - Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, + InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, account::AccountWithMetadata, program::{ChainedCall, InstructionData, ProgramId, ProgramOutput}, }; @@ -63,14 +62,10 @@ impl From for ProgramWithDependencies { /// Generates a proof of the execution of a NSSA program inside the privacy preserving execution /// circuit. -/// TODO: too many parameters. pub fn execute_and_prove( pre_states: Vec, instruction_data: InstructionData, - visibility_mask: Vec, - private_account_keys: Vec<(NullifierPublicKey, Identifier, SharedSecretKey)>, - private_account_nsks: Vec, - private_account_membership_proofs: Vec>, + account_identities: Vec, program_with_dependencies: &ProgramWithDependencies, ) -> Result<(PrivacyPreservingCircuitOutput, Proof), NssaError> { let ProgramWithDependencies { @@ -128,10 +123,7 @@ pub fn execute_and_prove( let circuit_input = PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + account_identities, program_id: program_with_dependencies.program.id(), }; @@ -184,7 +176,8 @@ mod tests { #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] use nssa_core::{ - Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, SharedSecretKey, + Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, + SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::PrivateAccountKind, program::PdaSeed, @@ -242,10 +235,14 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + identifier: 0, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -335,13 +332,21 @@ mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), 0, shared_secret_1), - (recipient_keys.npk(), 0, shared_secret_2), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: commitment_set + .get_proof_for(&commitment_sender) + .expect("sender's commitment must be in the set"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![commitment_set.get_proof_for(&commitment_sender), None], &program.into(), ) .unwrap(); @@ -404,10 +409,11 @@ mod tests { let result = execute_and_prove( vec![pre], instruction, - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &program_with_deps, ); @@ -431,10 +437,11 @@ mod tests { let (output, _proof) = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk, identifier, shared_secret.clone())], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret.clone(), + identifier, + }], &program.clone().into(), ) .unwrap(); @@ -491,10 +498,14 @@ mod tests { funder.clone(), ], Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![3, 0], - vec![(alice_npk, 0, alice_shared_0.clone())], - vec![], - vec![None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_0.clone(), + identifier: 0, + }, + InputAccountIdentity::Public, + ], &program_with_deps, ) .unwrap(); @@ -522,10 +533,14 @@ mod tests { funder.clone(), ], Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![3, 0], - vec![(alice_npk, 1, alice_shared_1.clone())], - vec![], - vec![None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_1.clone(), + identifier: 1, + }, + InputAccountIdentity::Public, + ], &program_with_deps, ) .unwrap(); @@ -561,13 +576,19 @@ mod tests { AccountWithMetadata::new(Account::default(), false, recipient_0_id), ], Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![3, 2], vec![ - (alice_npk, 0, alice_shared_0.clone()), - (recipient_keys.npk(), 0, SharedSecretKey::new(&[20; 32], &recipient_keys.vpk())), + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_0.clone(), + nsk: alice_keys.nsk, + membership_proof: proof_pda_0.expect("pda_0 commitment must be in the set"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[20; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![alice_keys.nsk], - vec![proof_pda_0, None], &program_with_deps, ) .unwrap(); @@ -587,13 +608,19 @@ mod tests { AccountWithMetadata::new(Account::default(), false, recipient_1_id), ], Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), - vec![3, 2], vec![ - (alice_npk, 1, alice_shared_1.clone()), - (recipient_keys.npk(), 1, SharedSecretKey::new(&[21; 32], &recipient_keys.vpk())), + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_1.clone(), + nsk: alice_keys.nsk, + membership_proof: proof_pda_1.expect("pda_1 commitment must be in the set"), + identifier: 1, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[21; 32], &recipient_keys.vpk()), + identifier: 1, + }, ], - vec![alice_keys.nsk], - vec![proof_pda_1, None], &program_with_deps, ) .unwrap(); @@ -601,4 +628,108 @@ mod tests { assert_eq!(output_spend_1.new_commitments.len(), 2); assert_eq!(output_spend_1.new_nullifiers.len(), 2); } + + /// Group PDA deposit: creates a new PDA and transfers balance from the + /// counterparty. Both accounts owned by `private_pda_spender`. + #[test] + fn group_pda_deposit() { + let program = Program::private_pda_spender(); + let noop = Program::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + // PDA (new, mask 3) + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); + + // Sender (mask 0, public, owned by this program, has balance) + let sender_id = AccountId::new([99; 32]); + let sender_pre = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 10000, + ..Account::default() + }, + true, + sender_id, + ); + + let noop_id = noop.id(); + let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into()); + + let instruction = Program::serialize_instruction((seed, noop_id, 500_u128, true)).unwrap(); + + // PDA is mask 3 (private PDA), sender is mask 0 (public). + // The noop chained call is required to establish the mask-3 (seed, npk) binding + // that the circuit enforces for private PDAs. Without a caller providing pda_seeds, + // the circuit's binding check rejects the account. + let result = execute_and_prove( + vec![pda_pre, sender_pre], + instruction, + vec![ + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + let (output, _proof) = result.expect("group PDA deposit should succeed"); + // Only PDA (mask 3) produces a commitment; sender (mask 0) is public. + assert_eq!(output.new_commitments.len(), 1); + } + + /// Group PDA spend binding: the noop chained call with `pda_seeds` establishes + /// the mask-3 binding for an existing-but-default PDA. Uses amount=0 because + /// testing with a pre-funded PDA requires a two-tx sequence with membership proofs. + #[test] + fn group_pda_spend_binding() { + let program = Program::private_pda_spender(); + let noop = Program::noop(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let shared_secret_pda = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); + + let bob_id = AccountId::new([88; 32]); + let bob_pre = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 10000, + ..Account::default() + }, + true, + bob_id, + ); + + let noop_id = noop.id(); + let program_with_deps = ProgramWithDependencies::new(program, [(noop_id, noop)].into()); + + let instruction = Program::serialize_instruction((seed, noop_id, 0_u128, false)).unwrap(); + + let result = execute_and_prove( + vec![pda_pre, bob_pre], + instruction, + vec![ + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + let (output, _proof) = result.expect("group PDA spend binding should succeed"); + assert_eq!(output.new_commitments.len(), 1); + } } diff --git a/nssa/src/program.rs b/nssa/src/program.rs index db83a1c4..85cd1ae8 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -313,6 +313,16 @@ mod tests { } } + #[must_use] + pub fn private_pda_spender() -> Self { + use test_program_methods::{PRIVATE_PDA_SPENDER_ELF, PRIVATE_PDA_SPENDER_ID}; + + Self { + id: PRIVATE_PDA_SPENDER_ID, + elf: PRIVATE_PDA_SPENDER_ELF.to_vec(), + } + } + #[must_use] pub fn two_pda_claimer() -> Self { use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID}; diff --git a/nssa/src/state.rs b/nssa/src/state.rs index 6285677e..078ce20a 100644 --- a/nssa/src/state.rs +++ b/nssa/src/state.rs @@ -362,8 +362,8 @@ pub mod tests { use std::collections::HashMap; use nssa_core::{ - BlockId, Commitment, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - Timestamp, + BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, + NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, program::{ @@ -1294,10 +1294,14 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], Program::serialize_instruction(balance_to_move).unwrap(), - vec![0, 2], - vec![(recipient_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret, + identifier: 0, + }, + ], &Program::authenticated_transfer_program().into(), ) .unwrap(); @@ -1343,13 +1347,21 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 2], vec![ - (sender_keys.npk(), 0, shared_secret_1), - (recipient_keys.npk(), 0, shared_secret_2), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret_1, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: shared_secret_2, + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment), None], &program.into(), ) .unwrap(); @@ -1398,10 +1410,17 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(balance_to_move).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), 0, shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], &program.into(), ) .unwrap(); @@ -1615,10 +1634,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1641,10 +1657,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(10_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1667,10 +1680,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1693,10 +1703,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(vec![0]).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1727,10 +1734,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(large_data).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1753,10 +1757,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1788,10 +1789,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(()).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1814,10 +1812,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(()).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -1849,10 +1844,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![0, 0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public, InputAccountIdentity::Public], &program.into(), ); @@ -1881,177 +1873,11 @@ pub mod tests { AccountId::new([1; 32]), ); - // Setting only one visibility mask for a circuit execution with two pre_state accounts. - let visibility_mask = [0]; + // Single account_identity entry for a circuit execution with two pre_state accounts. let result = execute_and_prove( vec![public_account_1, public_account_2], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![], - vec![], - vec![], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_nonces_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - - // Setting only one key for an execution with two private accounts. - let private_account_keys = [( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - )]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_commitment_proofs_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting no second commitment proof. - let private_account_membership_proofs = [Some((0, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - private_account_membership_proofs.to_vec(), - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_fails_if_insufficient_auth_keys_are_provided() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting no auth key for an execution with one non default private accounts. - let private_account_nsks = []; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -2075,33 +1901,26 @@ pub mod tests { let private_account_2 = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let private_account_keys = [ - // First private account is the sender - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - // Second private account is the recipient - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ]; - - // Setting the recipient key to authorize the sender. - // This should be set to the sender private account in - // a normal circumstance. The recipient can't authorize this. - let private_account_nsks = [recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![]))]; + // Setting the recipient nsk to authorize the sender. + // This should be set to the sender private account in a normal circumstance. + // `PrivateAuthorizedUpdate` derives npk from nsk and asserts equality with + // `pre_state.account_id`, so a mismatched nsk fails that check. let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: recipient_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, + ], &program.into(), ); @@ -2135,21 +1954,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2183,21 +2000,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2231,21 +2046,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2279,21 +2092,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2325,21 +2136,19 @@ pub mod tests { let result = execute_and_prove( vec![private_account_1, private_account_2], Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateUnauthorized { + npk: recipient_keys.npk(), + ssk: SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), + identifier: 0, + }, ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], &program.into(), ); @@ -2368,14 +2177,17 @@ pub mod tests { let private_pda_account = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32])); - let visibility_mask = [0, 3]; let result = execute_and_prove( vec![public_account_1, private_pda_account], Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![(npk, 0, shared_secret)], - vec![], - vec![None], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: u128::MAX, + }, + ], &program.into(), ); @@ -2401,10 +2213,11 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk, u128::MAX, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: u128::MAX, + }], &program.into(), ); @@ -2439,10 +2252,11 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction(seed).unwrap(), - vec![3], - vec![(npk_b, 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk: npk_b, + ssk: shared_secret, + identifier: u128::MAX, + }], &program.into(), ); @@ -2473,10 +2287,11 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), - vec![3], - vec![(npk, u128::MAX, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: u128::MAX, + }], &program_with_deps, ); @@ -2510,10 +2325,11 @@ pub mod tests { let result = execute_and_prove( vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), - vec![3], - vec![(npk, 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: u128::MAX, + }], &program_with_deps, ); @@ -2546,10 +2362,18 @@ pub mod tests { let result = execute_and_prove( vec![pre_a, pre_b], Program::serialize_instruction(seed).unwrap(), - vec![3, 3], - vec![(keys_a.npk(), 0, shared_a), (keys_b.npk(), 0, shared_b)], - vec![], - vec![None, None], + vec![ + InputAccountIdentity::PrivatePdaInit { + npk: keys_a.npk(), + ssk: shared_a, + identifier: u128::MAX, + }, + InputAccountIdentity::PrivatePdaInit { + npk: keys_b.npk(), + ssk: shared_b, + identifier: u128::MAX, + }, + ], &program.into(), ); @@ -2590,146 +2414,11 @@ pub mod tests { let result = execute_and_prove( vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), - vec![3], - vec![(npk, 0, shared_secret)], - vec![], - vec![None], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_nonces() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting three private account keys for a circuit execution with only two private - // accounts. - let private_account_keys = [ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[57; 32], &sender_keys.vpk()), - ), - ]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - vec![1, 2], - private_account_keys.to_vec(), - vec![sender_keys.nsk], - vec![Some((0, vec![]))], - &program.into(), - ); - - assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); - } - - #[test] - fn circuit_should_fail_with_too_many_private_account_auth_keys() { - let program = Program::simple_balance_transfer(); - let sender_keys = test_private_account_keys_1(); - let recipient_keys = test_private_account_keys_2(); - let private_account_1 = AccountWithMetadata::new( - Account { - program_owner: program.id(), - balance: 100, - ..Account::default() - }, - true, - (&sender_keys.npk(), 0), - ); - let private_account_2 = - AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - - // Setting two private account keys for a circuit execution with only one non default - // private account (visibility mask equal to 1 means that auth keys are expected). - let visibility_mask = [1, 2]; - let private_account_nsks = [sender_keys.nsk, recipient_keys.nsk]; - let private_account_membership_proofs = [Some((0, vec![])), Some((1, vec![]))]; - let result = execute_and_prove( - vec![private_account_1, private_account_2], - Program::serialize_instruction(10_u128).unwrap(), - visibility_mask.to_vec(), - vec![ - ( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[55; 32], &sender_keys.vpk()), - ), - ( - recipient_keys.npk(), - 0, - SharedSecretKey::new(&[56; 32], &recipient_keys.vpk()), - ), - ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: u128::MAX, + }], &program.into(), ); @@ -2806,20 +2495,24 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let visibility_mask = [1, 1]; - let private_account_nsks = [sender_keys.nsk, sender_keys.nsk]; - let private_account_membership_proofs = [Some((1, vec![])), Some((1, vec![]))]; let shared_secret = SharedSecretKey::new(&[55; 32], &sender_keys.vpk()); let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), - visibility_mask.to_vec(), vec![ - (sender_keys.npk(), 0, shared_secret), - (sender_keys.npk(), 0, shared_secret), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: (1, vec![]), + identifier: 0, + }, ], - private_account_nsks.to_vec(), - private_account_membership_proofs.to_vec(), &program.into(), ); @@ -3110,10 +2803,7 @@ pub mod tests { let result = execute_and_prove( vec![public_account], Program::serialize_instruction(0_u128).unwrap(), - vec![0], - vec![], - vec![], - vec![], + vec![InputAccountIdentity::Public], &program.into(), ); @@ -3152,10 +2842,17 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![sender_pre, recipient_pre], Program::serialize_instruction(37_u128).unwrap(), - vec![1, 0], - vec![(sender_keys.npk(), 0, shared_secret)], - vec![sender_keys.nsk], - vec![state.get_proof_for_commitment(&sender_commitment)], + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], &program.into(), ) .unwrap(); @@ -3273,12 +2970,23 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![to_account, from_account], Program::serialize_instruction(instruction).unwrap(), - vec![1, 1], - vec![(from_keys.npk(), 0, to_ss), (to_keys.npk(), 0, from_ss)], - vec![from_keys.nsk, to_keys.nsk], vec![ - state.get_proof_for_commitment(&from_commitment), - state.get_proof_for_commitment(&to_commitment), + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: to_ss, + nsk: from_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&from_commitment) + .expect("from's commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: from_ss, + nsk: to_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&to_commitment) + .expect("to's commitment must be in state"), + identifier: 0, + }, ], &program_with_deps, ) @@ -3542,10 +3250,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &program.into(), ) .unwrap(); @@ -3590,10 +3299,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(0_u128).unwrap(), - vec![2], - vec![(private_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: private_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &program.into(), ) .unwrap(); @@ -3642,10 +3352,11 @@ pub mod tests { let (output, proof) = execute_and_prove( vec![authorized_account.clone()], Program::serialize_instruction(balance).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk: private_keys.nsk, + identifier: 0, + }], &claimer_program.into(), ) .unwrap(); @@ -3688,10 +3399,11 @@ pub mod tests { let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), - vec![1], - vec![(private_keys.npk(), 0, shared_secret2)], - vec![private_keys.nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret2, + nsk: private_keys.nsk, + identifier: 0, + }], &noop_program.into(), ); @@ -3764,14 +3476,12 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3792,14 +3502,12 @@ pub mod tests { let result = execute_and_prove( vec![private_account], Program::serialize_instruction(instruction).unwrap(), - vec![1], - vec![( - sender_keys.npk(), - 0, - SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), - )], - vec![sender_keys.nsk], - vec![Some((0, vec![]))], + vec![InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: SharedSecretKey::new(&[3; 32], &sender_keys.vpk()), + nsk: sender_keys.nsk, + membership_proof: (0, vec![]), + identifier: 0, + }], &program.into(), ); @@ -3852,10 +3560,17 @@ pub mod tests { let result = execute_and_prove( vec![sender_account, recipient_account], Program::serialize_instruction(instruction).unwrap(), - vec![0, 1], - vec![(recipient_keys.npk(), 0, recipient)], - vec![recipient_keys.nsk], - vec![state.get_proof_for_commitment(&recipient_commitment)], + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: recipient, + nsk: recipient_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&recipient_commitment) + .expect("recipient's commitment must be in state"), + identifier: 0, + }, + ], &program_with_deps, ); @@ -4002,10 +3717,11 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &validity_window_program.into(), ) .unwrap(); @@ -4071,10 +3787,11 @@ pub mod tests { let (output, proof) = circuit::execute_and_prove( vec![pre], Program::serialize_instruction(instruction).unwrap(), - vec![2], - vec![(account_keys.npk(), 0, shared_secret)], - vec![], - vec![None], + vec![InputAccountIdentity::PrivateUnauthorized { + npk: account_keys.npk(), + ssk: shared_secret, + identifier: 0, + }], &validity_window_program.into(), ) .unwrap(); diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index ede43ae8..eb3c0e0f 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -5,9 +5,9 @@ use std::{ use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier, - PrivateAccountKind, - MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, + InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivateAccountKind, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, + SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ @@ -18,23 +18,24 @@ use nssa_core::{ }; use risc0_zkvm::{guest::env, serde::to_vec}; - /// State of the involved accounts before and after program execution. struct ExecutionState { pre_states: Vec, post_states: HashMap, block_validity_window: BlockValidityWindow, timestamp_validity_window: TimestampValidityWindow, - /// Positions (in `pre_states`) of mask-3 accounts whose supplied npk has been bound to + /// Positions (in `pre_states`) of private-PDA accounts whose supplied npk has been bound to /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk, identifier)` /// check. /// Two proof paths populate this set: a `Claim::Pda(seed)` in a program's `post_state` on /// that `pre_state`, or a caller's `ChainedCall.pda_seeds` entry matching that `pre_state` /// under the private derivation. Binding is an idempotent property, not an event: the same /// position can legitimately be bound through both paths in the same tx (e.g. a program - /// claims a private PDA and then delegates it to a callee), and the set uses `contains`, - /// not `assert!(insert)`. After the main loop, every mask-3 position must appear in this - /// set; otherwise the npk is unbound and the circuit rejects. + /// claims a private PDA and then delegates it to a callee), and the map uses `contains_key`, + /// not `assert!(insert)`. After the main loop, every private-PDA position must appear in this + /// map; otherwise the npk is unbound and the circuit rejects. + /// The stored `(ProgramId, PdaSeed)` is the owner program and seed, used in + /// `compute_circuit_output` to construct `PrivateAccountKind::Pda { program_id, seed, identifier }`. private_pda_bound_positions: HashMap, /// Across the whole transaction, each `(program_id, seed)` pair may resolve to at most one /// `AccountId`. A seed under a program can derive a family of accounts, one public PDA and @@ -45,39 +46,30 @@ struct ExecutionState { /// `AccountId` entry or as an equality check against the existing one, making the rule: one /// `(program, seed)` → one account per tx. pda_family_binding: HashMap<(ProgramId, PdaSeed), AccountId>, - /// Map from a mask-3 `pre_state`'s position in `visibility_mask` to the (npk, identifier) - /// supplied for that position in `private_account_keys`. Built once in `derive_from_outputs` - /// by walking `visibility_mask` in lock-step with `private_account_keys`, used later by the - /// claim and caller-seeds authorization paths. + /// Map from a private-PDA `pre_state`'s position in `account_identities` to the (npk, + /// identifier) supplied for that position. Built once in `derive_from_outputs` by walking + /// `account_identities` and consulting `npk_if_private_pda`. Used later by the claim and + /// caller-seeds authorization paths to verify + /// `AccountId::for_private_pda(program_id, seed, npk, identifier) == pre_state.account_id`. private_pda_npk_by_position: HashMap, } impl ExecutionState { /// Validate program outputs and derive the overall execution state. pub fn derive_from_outputs( - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], + account_identities: &[InputAccountIdentity], program_id: ProgramId, program_outputs: Vec, ) -> Self { - // Build position → npk map for mask-3 pre_states. `private_account_keys` is consumed in - // pre_state order across all masks 1/2/3, so walk `visibility_mask` in lock-step. The - // downstream `compute_circuit_output` also consumes the same iterator and its trailing - // assertions catch an over-supply of keys; under-supply surfaces here. - let mut private_pda_npk_by_position: HashMap = HashMap::new(); - { - let mut keys_iter = private_account_keys.iter(); - for (pos, &mask) in visibility_mask.iter().enumerate() { - if matches!(mask, 1..=3) { - let (npk, identifier, _) = keys_iter.next().unwrap_or_else(|| { - panic!( - "private_account_keys shorter than visibility_mask demands: no key for masked position {pos} (mask {mask})" - ) - }); - if mask == 3 { - private_pda_npk_by_position.insert(pos, (*npk, *identifier)); - } - } + // Build position → (npk, identifier) map for private-PDA pre_states, indexed by position + // in `account_identities`. The vec is documented as 1:1 with the program's pre_state + // order, so position here matches `pre_state_position` used downstream in + // `validate_and_sync_states`. + let mut private_pda_npk_by_position: HashMap = + HashMap::new(); + for (pos, account_identity) in account_identities.iter().enumerate() { + if let Some((npk, identifier)) = account_identity.npk_if_private_pda() { + private_pda_npk_by_position.insert(pos, (npk, identifier)); } } @@ -194,7 +186,7 @@ impl ExecutionState { } execution_state.validate_and_sync_states( - visibility_mask, + account_identities, chained_call.program_id, caller_program_id, &chained_call.pda_seeds, @@ -211,12 +203,12 @@ impl ExecutionState { "Inner call without a chained call found", ); - // Every mask-3 pre_state must have had its npk bound to its account_id, either via a - // `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` matching - // the private derivation. An unbound mask-3 pre_state has no cryptographic link between - // the supplied npk and the account_id, and must be rejected. - for (pos, &mask) in visibility_mask.iter().enumerate() { - if mask == 3 { + // Every private-PDA pre_state must have had its npk bound to its account_id, either via + // a `Claim::Pda(seed)` in some program's post_state or via a caller's `pda_seeds` + // matching the private derivation. An unbound private-PDA pre_state has no + // cryptographic link between the supplied npk and the account_id, and must be rejected. + for (pos, account_identity) in account_identities.iter().enumerate() { + if account_identity.is_private_pda() { assert!( execution_state.private_pda_bound_positions.contains_key(&pos), "private PDA pre_state at position {pos} has no proven (seed, npk) binding via Claim::Pda or caller pda_seeds" @@ -251,7 +243,7 @@ impl ExecutionState { /// Validate program pre and post states and populate the execution state. fn validate_and_sync_states( &mut self, - visibility_mask: &[u8], + account_identities: &[InputAccountIdentity], program_id: ProgramId, caller_program_id: Option, caller_pda_seeds: &[PdaSeed], @@ -329,9 +321,9 @@ impl ExecutionState { .position(|acc| acc.account_id == pre_account_id) .expect("Pre state must exist at this point"); - let mask = visibility_mask[pre_state_position]; - match mask { - 0 => match claim { + let account_identity = &account_identities[pre_state_position]; + if account_identity.is_public() { + match claim { Claim::Authorized => { // Note: no need to check authorized pdas because we have already // checked consistency of authorization above. @@ -353,35 +345,40 @@ impl ExecutionState { pre_account_id, ); } - }, - 3 => { - match claim { - Claim::Authorized => {} - Claim::Pda(seed) => { - let (npk, identifier) = self + } + } else if account_identity.is_private_pda() { + match claim { + Claim::Authorized => { + assert!( + pre_is_authorized, + "Cannot claim unauthorized private PDA {pre_account_id}" + ); + } + Claim::Pda(seed) => { + let (npk, identifier) = self .private_pda_npk_by_position .get(&pre_state_position) - .expect("private PDA pre_state must have an npk in the position map"); - let pda = AccountId::for_private_pda(&program_id, &seed, npk, *identifier); - assert_eq!( - pre_account_id, pda, - "Invalid private PDA claim for account {pre_account_id}" + .expect( + "private PDA pre_state must have an npk in the position map", ); - self.private_pda_bound_positions.insert(pre_state_position, (program_id, seed)); - assert_family_binding( - &mut self.pda_family_binding, - program_id, - seed, - pre_account_id, - ); - } + let pda = AccountId::for_private_pda(&program_id, &seed, npk, *identifier); + assert_eq!( + pre_account_id, pda, + "Invalid private PDA claim for account {pre_account_id}" + ); + self.private_pda_bound_positions.insert(pre_state_position, (program_id, seed)); + assert_family_binding( + &mut self.pda_family_binding, + program_id, + seed, + pre_account_id, + ); } } - _ => { - // Mask 1/2: standard private accounts don't enforce the claim semantics. - // Unauthorized private claiming is intentionally allowed since operating - // these accounts requires the npk/nsk keypair anyway. - } + } else { + // Standalone private accounts: don't enforce the claim semantics. + // Unauthorized private claiming is intentionally allowed since operating + // these accounts requires the npk/nsk keypair anyway. } post.account_mut().program_owner = program_id; @@ -483,10 +480,7 @@ fn resolve_authorization_and_record_bindings( fn compute_circuit_output( mut execution_state: ExecutionState, - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], - private_account_nsks: &[NullifierSecretKey], - private_account_membership_proofs: &[Option], + account_identities: &[InputAccountIdentity], ) -> PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput { public_pre_states: Vec::new(), @@ -501,287 +495,273 @@ fn compute_circuit_output( let pda_seed_by_position = std::mem::take(&mut execution_state.private_pda_bound_positions); let states_iter = execution_state.into_states_iter(); assert_eq!( - visibility_mask.len(), + account_identities.len(), states_iter.len(), - "Invalid visibility mask length" + "Invalid account_identities length" ); - let mut private_keys_iter = private_account_keys.iter(); - let mut private_nsks_iter = private_account_nsks.iter(); - let mut private_membership_proofs_iter = private_account_membership_proofs.iter(); - let mut output_index = 0; - for (pos, (account_visibility_mask, (pre_state, post_state))) in - visibility_mask.iter().copied().zip(states_iter).enumerate() + for (pos, (account_identity, (pre_state, post_state))) in + account_identities.iter().zip(states_iter).enumerate() { - match account_visibility_mask { - 0 => { - // Public account + match account_identity { + InputAccountIdentity::Public => { output.public_pre_states.push(pre_state); output.public_post_states.push(post_state); } - 1 | 2 => { - let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; + InputAccountIdentity::PrivateAuthorizedInit { + ssk, + nsk, + identifier, + } => { + let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); + + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values" + ); + + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk, + nsk, + membership_proof, + identifier, + } => { + let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::from((&npk, *identifier)); + + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" + ); + + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &account_id, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivateUnauthorized { + npk, + ssk, + identifier, + } => { let account_id = AccountId::from((npk, *identifier)); assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); - - let (new_nullifier, new_nonce) = if account_visibility_mask == 1 { - // Private account with authentication - - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - - // Verify the nullifier public key - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - // Check pre_state authorization - assert!( - pre_state.is_authorized, - "Pre-state not authorized for authenticated private account" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - &account_id, - nsk, - ); - - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - - (new_nullifier, new_nonce) - } else { - // Private account without authentication - - assert_eq!( - pre_state.account, - Account::default(), - "Found new private account with non default values", - ); - - assert!( - !pre_state.is_authorized, - "Found new private account marked as authorized." - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for unauthorized accounts" - ); - - let nullifier = Nullifier::for_account_initialization(&account_id); - - let new_nonce = Nonce::private_account_nonce_init(&account_id); - - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - // Update post-state with new nonce - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - // Compute commitment - let commitment_post = Commitment::new(&account_id, &post_with_updated_nonce); - - // Encrypt and push post state - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - &PrivateAccountKind::Regular(*identifier), - shared_secret, - &commitment_post, - output_index, + assert_eq!( + pre_state.account, + Account::default(), + "Found new private account with non default values", + ); + assert!( + !pre_state.is_authorized, + "Found new private account marked as authorized." ); - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = Nonce::private_account_nonce_init(&account_id); + + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Regular(*identifier), + ssk, + new_nullifier, + new_nonce, + ); } - 3 => { - // Private PDA account. The supplied npk has already been bound to - // `pre_state.account_id` upstream in `validate_and_sync_states`, either via a - // `Claim::Pda(seed)` match or via a caller `pda_seeds` match, both of which - // assert `AccountId::for_private_pda(owner, seed, npk, identifier) == account_id`. The - // post-loop assertion in `derive_from_outputs` (see the - // `private_pda_bound_positions` check) guarantees that every mask-3 - // position has been through at least one such binding, so this - // branch can safely use the wallet npk without re-verifying. - let Some((npk, identifier, shared_secret)) = private_keys_iter.next() else { - panic!("Missing private account key"); - }; + InputAccountIdentity::PrivatePdaInit { npk: _, ssk, identifier } => { + // The npk-to-account_id binding is established upstream in + // `validate_and_sync_states` via `Claim::Pda(seed)` or a caller `pda_seeds` + // match. Here we only enforce the init pre-conditions. The supplied npk on + // the variant has been recorded into `private_pda_npk_by_position` and used + // for the binding check; we use `pre_state.account_id` directly for nullifier + // and commitment derivation. + assert!( + !pre_state.is_authorized, + "PrivatePdaInit requires unauthorized pre_state" + ); + assert_eq!( + pre_state.account, + Account::default(), + "New private PDA must be default" + ); - let (new_nullifier, new_nonce) = if pre_state.is_authorized { - // Existing private PDA with authentication (like mask 1) - let Some(nsk) = private_nsks_iter.next() else { - panic!("Missing private account nullifier secret key"); - }; - assert_eq!( - npk, - &NullifierPublicKey::from(nsk), - "Nullifier public key mismatch" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - - let new_nullifier = compute_nullifier_and_set_digest( - membership_proof_opt.as_ref(), - &pre_state.account, - &pre_state.account_id, - nsk, - ); - let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - (new_nullifier, new_nonce) - } else { - // New private PDA (like mask 2). The default + unauthorized requirement - // here rules out use cases like a fully-private multisig, which would need - // a non-default, non-authorized private PDA input account. - // TODO(private-pdas-pr-2/3): relax this once the wallet can supply a - // `(seed, owner)` side input so the npk-to-account_id binding can be - // re-verified for an existing private PDA without a `Claim::Pda` or caller - // `pda_seeds` match. - assert_eq!( - pre_state.account, - Account::default(), - "New private PDA must be default" - ); - - let Some(membership_proof_opt) = private_membership_proofs_iter.next() else { - panic!("Missing membership proof"); - }; - assert!( - membership_proof_opt.is_none(), - "Membership proof must be None for new accounts" - ); - - let nullifier = Nullifier::for_account_initialization(&pre_state.account_id); - let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); - ((nullifier, DUMMY_COMMITMENT_HASH), new_nonce) - }; - output.new_nullifiers.push(new_nullifier); - - let mut post_with_updated_nonce = post_state; - post_with_updated_nonce.nonce = new_nonce; - - let commitment_post = - Commitment::new(&pre_state.account_id, &post_with_updated_nonce); + let new_nullifier = ( + Nullifier::for_account_initialization(&pre_state.account_id), + DUMMY_COMMITMENT_HASH, + ); + let new_nonce = Nonce::private_account_nonce_init(&pre_state.account_id); + let account_id = pre_state.account_id; let (pda_program_id, seed) = pda_seed_by_position .get(&pos) - .expect("mask-3 position must be in pda_seed_by_position"); - let encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, + .expect("PrivatePdaInit position must be in pda_seed_by_position"); + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, &PrivateAccountKind::Pda { program_id: *pda_program_id, seed: *seed, identifier: *identifier, }, - shared_secret, - &commitment_post, - output_index, + ssk, + new_nullifier, + new_nonce, + ); + } + InputAccountIdentity::PrivatePdaUpdate { + ssk, + nsk, + membership_proof, + identifier, + } => { + // The npk binding is established upstream. Authorization must already be set; + // an unauthorized PrivatePdaUpdate would mean the prover supplied an nsk for an + // unbound PDA, which the upstream binding check would have rejected anyway, + // but we assert here to fail fast and document the precondition. + assert!( + pre_state.is_authorized, + "PrivatePdaUpdate requires authorized pre_state" ); - output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); - output_index = output_index - .checked_add(1) - .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); + let new_nullifier = compute_update_nullifier_and_set_digest( + membership_proof, + &pre_state.account, + &pre_state.account_id, + nsk, + ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); + + let account_id = pre_state.account_id; + let (pda_program_id, seed) = pda_seed_by_position + .get(&pos) + .expect("PrivatePdaUpdate position must be in pda_seed_by_position"); + emit_private_output( + &mut output, + &mut output_index, + post_state, + &account_id, + &PrivateAccountKind::Pda { + program_id: *pda_program_id, + seed: *seed, + identifier: *identifier, + }, + ssk, + new_nullifier, + new_nonce, + ); } - _ => panic!("Invalid visibility mask value"), } } - assert!( - private_keys_iter.next().is_none(), - "Too many private account keys" - ); - - assert!( - private_nsks_iter.next().is_none(), - "Too many private account nullifier secret keys" - ); - - assert!( - private_membership_proofs_iter.next().is_none(), - "Too many private account membership proofs" - ); - output } -fn compute_nullifier_and_set_digest( - membership_proof_opt: Option<&MembershipProof>, +#[expect( + clippy::too_many_arguments, + reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial" +)] +fn emit_private_output( + output: &mut PrivacyPreservingCircuitOutput, + output_index: &mut u32, + post_state: Account, + account_id: &AccountId, + kind: &PrivateAccountKind, + shared_secret: &SharedSecretKey, + new_nullifier: (Nullifier, CommitmentSetDigest), + new_nonce: Nonce, +) { + output.new_nullifiers.push(new_nullifier); + + let mut post_with_updated_nonce = post_state; + post_with_updated_nonce.nonce = new_nonce; + + let commitment_post = Commitment::new(account_id, &post_with_updated_nonce); + let encrypted_account = EncryptionScheme::encrypt( + &post_with_updated_nonce, + kind, + shared_secret, + &commitment_post, + *output_index, + ); + + output.new_commitments.push(commitment_post); + output.ciphertexts.push(encrypted_account); + *output_index = output_index + .checked_add(1) + .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); +} + +fn compute_update_nullifier_and_set_digest( + membership_proof: &MembershipProof, pre_account: &Account, account_id: &AccountId, nsk: &NullifierSecretKey, ) -> (Nullifier, CommitmentSetDigest) { - membership_proof_opt.as_ref().map_or_else( - || { - assert_eq!( - *pre_account, - Account::default(), - "Found new private account with non default values" - ); - - // Compute initialization nullifier - let nullifier = Nullifier::for_account_initialization(account_id); - (nullifier, DUMMY_COMMITMENT_HASH) - }, - |membership_proof| { - // Compute commitment set digest associated with provided auth path - let commitment_pre = Commitment::new(account_id, pre_account); - let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); - - // Compute update nullifier - let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); - (nullifier, set_digest) - }, - ) + let commitment_pre = Commitment::new(account_id, pre_account); + let set_digest = compute_digest_for_path(&commitment_pre, membership_proof); + let nullifier = Nullifier::for_account_update(&commitment_pre, nsk); + (nullifier, set_digest) } fn main() { let PrivacyPreservingCircuitInput { program_outputs, - visibility_mask, - private_account_keys, - private_account_nsks, - private_account_membership_proofs, + account_identities, program_id, } = env::read(); - let execution_state = ExecutionState::derive_from_outputs( - &visibility_mask, - &private_account_keys, - program_id, - program_outputs, - ); + let execution_state = + ExecutionState::derive_from_outputs(&account_identities, program_id, program_outputs); - let output = compute_circuit_output( - execution_state, - &visibility_mask, - &private_account_keys, - &private_account_nsks, - &private_account_membership_proofs, - ); + let output = compute_circuit_output(execution_state, &account_identities); env::commit(&output); } diff --git a/sequencer/core/Cargo.toml b/sequencer/core/Cargo.toml index efd0e359..827c8b2e 100644 --- a/sequencer/core/Cargo.toml +++ b/sequencer/core/Cargo.toml @@ -13,7 +13,7 @@ nssa_core.workspace = true common.workspace = true storage.workspace = true mempool.workspace = true -bedrock_client.workspace = true +logos-blockchain-zone-sdk.workspace = true testnet_initial_state.workspace = true anyhow.workspace = true @@ -30,7 +30,6 @@ rand.workspace = true borsh.workspace = true bytesize.workspace = true url.workspace = true -jsonrpsee = { workspace = true, features = ["ws-client"] } [features] default = [] diff --git a/sequencer/core/src/block_publisher.rs b/sequencer/core/src/block_publisher.rs new file mode 100644 index 00000000..9f4c8235 --- /dev/null +++ b/sequencer/core/src/block_publisher.rs @@ -0,0 +1,136 @@ +use std::{sync::Arc, time::Duration}; + +use anyhow::{Context as _, Result, anyhow}; +use common::block::Block; +use log::warn; +pub use logos_blockchain_core::mantle::ops::channel::MsgId; +pub use logos_blockchain_key_management_system_service::keys::Ed25519Key; +pub use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; +use logos_blockchain_zone_sdk::{ + CommonHttpClient, + adapter::NodeHttpClient, + sequencer::{Event, SequencerConfig as ZoneSdkSequencerConfig, SequencerHandle, ZoneSequencer}, + state::InscriptionInfo, +}; +use tokio::task::JoinHandle; + +use crate::config::BedrockConfig; + +/// Sink for `Event::Published` checkpoints emitted by the drive task. +/// Caller is responsible for persistence (e.g. writing to rocksdb). +pub type CheckpointSink = Box; + +/// Sink for finalized L2 block ids derived from `Event::TxsFinalized` and +/// `Event::FinalizedInscriptions`. Caller is responsible for cleanup +/// (e.g. marking pending blocks as finalized in storage). +pub type FinalizedBlockSink = Box; + +#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")] +pub trait BlockPublisherTrait: Clone { + async fn new( + config: &BedrockConfig, + bedrock_signing_key: Ed25519Key, + resubmit_interval: Duration, + initial_checkpoint: Option, + on_checkpoint: CheckpointSink, + on_finalized_block: FinalizedBlockSink, + ) -> Result; + + /// Fire-and-forget publish. Zone-sdk drives the actual submission and + /// retries internally; this just hands the payload off. + async fn publish_block(&self, block: &Block) -> Result<()>; +} + +/// Real block publisher backed by zone-sdk's `ZoneSequencer`. +#[derive(Clone)] +pub struct ZoneSdkPublisher { + handle: SequencerHandle, + // Aborts the drive task when the last clone is dropped. + _drive_task: Arc, +} + +struct DriveTaskGuard(JoinHandle<()>); + +impl Drop for DriveTaskGuard { + fn drop(&mut self) { + self.0.abort(); + } +} + +impl BlockPublisherTrait for ZoneSdkPublisher { + async fn new( + config: &BedrockConfig, + bedrock_signing_key: Ed25519Key, + resubmit_interval: Duration, + initial_checkpoint: Option, + on_checkpoint: CheckpointSink, + on_finalized_block: FinalizedBlockSink, + ) -> Result { + let basic_auth = config.auth.clone().map(Into::into); + let node = NodeHttpClient::new(CommonHttpClient::new(basic_auth), config.node_url.clone()); + + let zone_sdk_config = ZoneSdkSequencerConfig { + resubmit_interval, + ..ZoneSdkSequencerConfig::default() + }; + + let (mut sequencer, mut handle) = ZoneSequencer::init_with_config( + config.channel_id, + bedrock_signing_key, + node, + zone_sdk_config, + initial_checkpoint, + ); + + let drive_task = tokio::spawn(async move { + loop { + let Some(event) = sequencer.next_event().await else { + continue; + }; + match event { + Event::Published { checkpoint, .. } => on_checkpoint(checkpoint), + Event::TxsFinalized { inscriptions, .. } + | Event::FinalizedInscriptions { inscriptions } => { + if let Some(max_block_id) = max_block_id_from_inscriptions(&inscriptions) { + on_finalized_block(max_block_id); + } + } + Event::ChannelUpdate { .. } | Event::Ready => {} + } + } + }); + + handle.wait_ready().await; + + Ok(Self { + handle, + _drive_task: Arc::new(DriveTaskGuard(drive_task)), + }) + } + + async fn publish_block(&self, block: &Block) -> Result<()> { + let data = borsh::to_vec(block).context("Failed to serialize block")?; + self.handle + .publish_message(data) + .await + .map_err(|e| anyhow!("zone-sdk publish failed: {e}"))?; + Ok(()) + } +} + +/// Deserialize each inscription payload as a `Block` and return the highest +/// `block_id`. Bad payloads are logged and skipped. +fn max_block_id_from_inscriptions(inscriptions: &[InscriptionInfo]) -> Option { + inscriptions + .iter() + .filter_map( + |inscription| match borsh::from_slice::(&inscription.payload) { + Ok(block) => Some(block.header.block_id), + Err(err) => { + warn!("Failed to deserialize finalized inscription as Block: {err:#}"); + None + } + }, + ) + .max() +} diff --git a/sequencer/core/src/block_settlement_client.rs b/sequencer/core/src/block_settlement_client.rs deleted file mode 100644 index 6b32f8de..00000000 --- a/sequencer/core/src/block_settlement_client.rs +++ /dev/null @@ -1,116 +0,0 @@ -use anyhow::{Context as _, Result}; -use bedrock_client::BedrockClient; -pub use common::block::Block; -pub use logos_blockchain_core::mantle::{MantleTx, SignedMantleTx, ops::channel::MsgId}; -use logos_blockchain_core::mantle::{ - Op, OpProof, Transaction as _, - ops::channel::{ChannelId, inscribe::InscriptionOp}, -}; -pub use logos_blockchain_key_management_system_service::keys::Ed25519Key; -use logos_blockchain_key_management_system_service::keys::Ed25519PublicKey; - -use crate::config::BedrockConfig; - -#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")] -pub trait BlockSettlementClientTrait: Clone { - //// Create a new client. - fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result; - - /// Get the bedrock channel ID used by this client. - fn bedrock_channel_id(&self) -> ChannelId; - - /// Get the bedrock signing key used by this client. - fn bedrock_signing_key(&self) -> &Ed25519Key; - - /// Post a transaction to the node. - async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()>; - - /// Create and sign a transaction for inscribing data. - fn create_inscribe_tx(&self, block: &Block) -> Result<(SignedMantleTx, MsgId)> { - let inscription_data = borsh::to_vec(block)?; - log::debug!( - "The size of the block {} is {} bytes", - block.header.block_id, - inscription_data.len() - ); - let verifying_key_bytes = self.bedrock_signing_key().public_key().to_bytes(); - let verifying_key = - Ed25519PublicKey::from_bytes(&verifying_key_bytes).expect("valid ed25519 public key"); - - let inscribe_op = InscriptionOp { - channel_id: self.bedrock_channel_id(), - inscription: inscription_data, - parent: block.bedrock_parent_id.into(), - signer: verifying_key, - }; - let inscribe_op_id = inscribe_op.id(); - - let inscribe_tx = MantleTx { - ops: vec![Op::ChannelInscribe(inscribe_op)], - // Altruistic test config - storage_gas_price: 0.into(), - execution_gas_price: 0.into(), - }; - - let tx_hash = inscribe_tx.hash(); - let signature_bytes = self - .bedrock_signing_key() - .sign_payload(tx_hash.as_signing_bytes().as_ref()) - .to_bytes(); - let signature = - logos_blockchain_key_management_system_service::keys::Ed25519Signature::from_bytes( - &signature_bytes, - ); - - let signed_mantle_tx = SignedMantleTx { - ops_proofs: vec![OpProof::Ed25519Sig(signature)], - mantle_tx: inscribe_tx, - }; - Ok((signed_mantle_tx, inscribe_op_id)) - } -} - -/// A component that posts block data to logos blockchain. -#[derive(Clone)] -pub struct BlockSettlementClient { - client: BedrockClient, - signing_key: Ed25519Key, - channel_id: ChannelId, -} - -impl BlockSettlementClientTrait for BlockSettlementClient { - fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result { - let client = - BedrockClient::new(config.backoff, config.node_url.clone(), config.auth.clone()) - .context("Failed to initialize bedrock client")?; - Ok(Self { - client, - signing_key, - channel_id: config.channel_id, - }) - } - - async fn submit_inscribe_tx_to_bedrock(&self, tx: SignedMantleTx) -> Result<()> { - let (parent_id, msg_id) = match tx.mantle_tx.ops.first() { - Some(Op::ChannelInscribe(inscribe)) => (inscribe.parent, inscribe.id()), - _ => panic!("Expected ChannelInscribe op"), - }; - self.client - .post_transaction(tx) - .await - .context("Failed to post transaction to Bedrock after retries")? - .context("Failed to post transaction to Bedrock with non-retryable error")?; - - log::debug!("Posted block to Bedrock with parent id {parent_id:?} and msg id: {msg_id:?}"); - - Ok(()) - } - - fn bedrock_channel_id(&self) -> ChannelId { - self.channel_id - } - - fn bedrock_signing_key(&self) -> &Ed25519Key { - &self.signing_key - } -} diff --git a/sequencer/core/src/block_store.rs b/sequencer/core/src/block_store.rs index 7e47005d..e85b5d33 100644 --- a/sequencer/core/src/block_store.rs +++ b/sequencer/core/src/block_store.rs @@ -1,16 +1,17 @@ -use std::{collections::HashMap, path::Path}; +use std::{collections::HashMap, path::Path, sync::Arc}; -use anyhow::Result; +use anyhow::{Context as _, Result}; use common::{ HashType, block::{Block, BlockMeta, MantleMsgId}, transaction::NSSATransaction, }; +use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; use nssa::V03State; use storage::{error::DbError, sequencer::RocksDBIO}; pub struct SequencerStore { - dbio: RocksDBIO, + dbio: Arc, // TODO: Consider adding the hashmap to the database for faster recovery. tx_hash_to_block_map: HashMap, genesis_id: u64, @@ -30,7 +31,11 @@ impl SequencerStore { ) -> Result { let tx_hash_to_block_map = block_to_transactions_map(genesis_block); - let dbio = RocksDBIO::open_or_create(location, genesis_block, genesis_msg_id)?; + let dbio = Arc::new(RocksDBIO::open_or_create( + location, + genesis_block, + genesis_msg_id, + )?); let genesis_id = dbio.get_meta_first_block_in_db()?; @@ -42,6 +47,14 @@ impl SequencerStore { }) } + /// Shared handle to the underlying rocksdb. Used to persist the zone-sdk + /// checkpoint from the sequencer's drive task without needing &mut to the + /// store. + #[must_use] + pub fn dbio(&self) -> Arc { + Arc::clone(&self.dbio) + } + pub fn get_block_at_id(&self, id: u64) -> Result, DbError> { self.dbio.get_block(id) } @@ -55,6 +68,7 @@ impl SequencerStore { } /// Returns the transaction corresponding to the given hash, if it exists in the blockchain. + #[must_use] pub fn get_transaction_by_hash(&self, hash: HashType) -> Option { let block_id = *self.tx_hash_to_block_map.get(&hash)?; let block = self @@ -76,10 +90,12 @@ impl SequencerStore { Ok(self.dbio.latest_block_meta()?) } + #[must_use] pub const fn genesis_id(&self) -> u64 { self.genesis_id } + #[must_use] pub const fn signing_key(&self) -> &nssa::PrivateKey { &self.signing_key } @@ -100,9 +116,26 @@ impl SequencerStore { Ok(()) } + #[must_use] pub fn get_nssa_state(&self) -> Option { self.dbio.get_nssa_state().ok() } + + pub fn get_zone_checkpoint(&self) -> Result> { + let Some(bytes) = self.dbio.get_zone_sdk_checkpoint_bytes()? else { + return Ok(None); + }; + let checkpoint: SequencerCheckpoint = serde_json::from_slice(&bytes) + .context("Failed to deserialize stored zone-sdk checkpoint")?; + Ok(Some(checkpoint)) + } + + pub fn set_zone_checkpoint(&self, checkpoint: &SequencerCheckpoint) -> Result<()> { + let bytes = + serde_json::to_vec(checkpoint).context("Failed to serialize zone-sdk checkpoint")?; + self.dbio.put_zone_sdk_checkpoint_bytes(&bytes)?; + Ok(()) + } } pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap { diff --git a/sequencer/core/src/config.rs b/sequencer/core/src/config.rs index fa4a2fa7..b33dd694 100644 --- a/sequencer/core/src/config.rs +++ b/sequencer/core/src/config.rs @@ -6,7 +6,6 @@ use std::{ }; use anyhow::Result; -use bedrock_client::BackoffConfig; use bytesize::ByteSize; use common::config::BasicAuth; use humantime_serde; @@ -42,8 +41,6 @@ pub struct SequencerConfig { pub signing_key: [u8; 32], /// Bedrock configuration options. pub bedrock_config: BedrockConfig, - /// Indexer RPC URL. - pub indexer_rpc_url: Url, #[serde(skip_serializing_if = "Option::is_none")] pub initial_public_accounts: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -52,9 +49,6 @@ pub struct SequencerConfig { #[derive(Clone, Serialize, Deserialize)] pub struct BedrockConfig { - /// Fibonacci backoff retry strategy configuration. - #[serde(default)] - pub backoff: BackoffConfig, /// Bedrock channel ID. pub channel_id: ChannelId, /// Bedrock Url. diff --git a/sequencer/core/src/indexer_client.rs b/sequencer/core/src/indexer_client.rs deleted file mode 100644 index 960b77a4..00000000 --- a/sequencer/core/src/indexer_client.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{ops::Deref, sync::Arc}; - -use anyhow::{Context as _, Result}; -use log::info; -pub use url::Url; - -#[expect(async_fn_in_trait, reason = "We don't care about Send/Sync here")] -pub trait IndexerClientTrait: Clone { - async fn new(indexer_url: &Url) -> Result; -} - -#[derive(Clone)] -pub struct IndexerClient(Arc); - -impl IndexerClientTrait for IndexerClient { - async fn new(indexer_url: &Url) -> Result { - info!("Connecting to Indexer at {indexer_url}"); - - let client = jsonrpsee::ws_client::WsClientBuilder::default() - .build(indexer_url) - .await - .context("Failed to create websocket client")?; - - Ok(Self(Arc::new(client))) - } -} - -impl Deref for IndexerClient { - type Target = jsonrpsee::ws_client::WsClient; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/sequencer/core/src/lib.rs b/sequencer/core/src/lib.rs index 22c09d85..bce8151f 100644 --- a/sequencer/core/src/lib.rs +++ b/sequencer/core/src/lib.rs @@ -1,7 +1,6 @@ use std::{path::Path, time::Instant}; use anyhow::{Context as _, Result, anyhow}; -use bedrock_client::SignedMantleTx; #[cfg(feature = "testnet")] use common::PINATA_BASE58; use common::{ @@ -20,33 +19,27 @@ pub use storage::error::DbError; use testnet_initial_state::initial_state; use crate::{ - block_settlement_client::{BlockSettlementClient, BlockSettlementClientTrait, MsgId}, + block_publisher::{BlockPublisherTrait, ZoneSdkPublisher}, block_store::SequencerStore, - indexer_client::{IndexerClient, IndexerClientTrait}, }; -pub mod block_settlement_client; +pub mod block_publisher; pub mod block_store; pub mod config; -pub mod indexer_client; #[cfg(feature = "mock")] pub mod mock; -pub struct SequencerCore< - BC: BlockSettlementClientTrait = BlockSettlementClient, - IC: IndexerClientTrait = IndexerClient, -> { +pub struct SequencerCore { state: nssa::V03State, store: SequencerStore, mempool: MemPool, sequencer_config: SequencerConfig, chain_height: u64, - block_settlement_client: BC, - indexer_client: IC, + block_publisher: BP, } -impl SequencerCore { +impl SequencerCore { /// Starts the sequencer using the provided configuration. /// If an existing database is found, the sequencer state is loaded from it and /// assumed to represent the correct latest state consistent with Bedrock-finalized data. @@ -70,23 +63,16 @@ impl SequencerCore SequencerCore b, + Err(err) => { + error!("Failed to serialize zone-sdk checkpoint: {err:#}"); + return; + } + }; + if let Err(err) = dbio_for_checkpoint.put_zone_sdk_checkpoint_bytes(&bytes) { + error!("Failed to persist zone-sdk checkpoint: {err:#}"); + } + }); + + let dbio_for_finalized = store.dbio(); + let on_finalized_block: block_publisher::FinalizedBlockSink = Box::new(move |block_id| { + if let Err(err) = dbio_for_finalized.clean_pending_blocks_up_to(block_id) { + error!("Failed to mark pending blocks finalized up to {block_id}: {err:#}"); + } + }); + + let block_publisher = BP::new( + &config.bedrock_config, + bedrock_signing_key, + config.retry_pending_blocks_timeout, + initial_checkpoint, + on_checkpoint, + on_finalized_block, + ) + .await + .expect("Failed to initialize Block Publisher"); + + // On a truly fresh start (no checkpoint persisted yet), publish the + // genesis block so the indexer can find the channel start. After the + // first publish, zone-sdk's checkpoint persistence covers further + // restarts. + if is_fresh_start && let Err(err) = block_publisher.publish_block(&genesis_block).await { + error!("Failed to publish genesis block: {err:#}"); + } + #[cfg_attr(not(feature = "testnet"), allow(unused_mut))] let mut state = if let Some(state) = store.get_nssa_state() { info!("Found local database. Loading state and pending blocks from it."); @@ -159,35 +190,33 @@ impl SequencerCore Result { - let (tx, _msg_id) = self - .produce_new_block_with_mempool_transactions() - .context("Failed to produce new block with mempool transactions")?; - match self - .block_settlement_client - .submit_inscribe_tx_to_bedrock(tx) - .await - { - Ok(()) => {} - Err(err) => { - error!("Failed to post block data to Bedrock with error: {err:#}"); - } + let block = self + .build_block_from_mempool() + .context("Failed to build block from mempool transactions")?; + + // TODO: Remove msg_id from store.update — it is no longer needed now that + // zone-sdk manages L1 settlement state via its own checkpoint. + let placeholder_msg_id = [0_u8; 32]; + + if let Err(err) = self.block_publisher.publish_block(&block).await { + error!("Failed to publish block to Bedrock with error: {err:#}"); } + self.store.update(&block, placeholder_msg_id, &self.state)?; Ok(self.chain_height) } - /// Produces new block from transactions in mempool and packs it into a `SignedMantleTx`. - pub fn produce_new_block_with_mempool_transactions( - &mut self, - ) -> Result<(SignedMantleTx, MsgId)> { + /// Builds a new block from transactions in the mempool. + /// Does NOT publish or store the block — the caller is responsible for that. + pub fn build_block_from_mempool(&mut self) -> Result { let now = Instant::now(); let new_block_height = self.next_block_id(); @@ -277,21 +306,12 @@ impl SequencerCore SequencerCore &nssa::V03State { @@ -319,22 +339,19 @@ impl SequencerCore Result<()> { - self.get_pending_blocks()? - .iter() - .map(|block| block.header.block_id) - .min() - .map_or(Ok(()), |first_pending_block_id| { - info!("Clearing pending blocks up to id: {last_finalized_block_id}"); - // TODO: Delete blocks instead of marking them as finalized. - // Current approach is used because we still have `GetBlockDataRequest`. - (first_pending_block_id..=last_finalized_block_id) - .try_for_each(|id| self.store.mark_block_as_finalized(id)) - }) + /// Marks all pending blocks with `block_id <= last_finalized_block_id` as + /// finalized. Idempotent. Production callers don't invoke this directly — + /// it's wired up in `start_from_config` to the publisher's + /// `on_finalized_block` sink, which fires on `Event::TxsFinalized` / + /// `Event::FinalizedInscriptions`. Kept on the type for tests. + // TODO: Delete blocks instead of marking them as finalized. Current + // approach is used because we still have `GetBlockDataRequest`. + pub fn clean_finalized_blocks_from_db(&self, last_finalized_block_id: u64) -> Result<()> { + info!("Clearing pending blocks up to id: {last_finalized_block_id}"); + self.store + .dbio() + .clean_pending_blocks_up_to(last_finalized_block_id)?; + Ok(()) } /// Returns the list of stored pending blocks. @@ -348,12 +365,8 @@ impl SequencerCore BC { - self.block_settlement_client.clone() - } - - pub fn indexer_client(&self) -> IC { - self.indexer_client.clone() + pub fn block_publisher(&self) -> BP { + self.block_publisher.clone() } fn next_block_id(&self) -> u64 { @@ -392,7 +405,6 @@ mod tests { use std::{pin::pin, time::Duration}; - use bedrock_client::BackoffConfig; use common::{ test_utils::sequencer_sign_key_for_testing, transaction::{NSSATransaction, clock_invocation}, @@ -420,16 +432,11 @@ mod tests { block_create_timeout: Duration::from_secs(1), signing_key: *sequencer_sign_key_for_testing().value(), bedrock_config: BedrockConfig { - backoff: BackoffConfig { - start_delay: Duration::from_millis(100), - max_retries: 5, - }, channel_id: ChannelId::from([0; 32]), node_url: "http://not-used-in-unit-tests".parse().unwrap(), auth: None, }, retry_pending_blocks_timeout: Duration::from_mins(4), - indexer_rpc_url: "ws://localhost:8779".parse().unwrap(), initial_public_accounts: None, initial_private_accounts: None, } @@ -457,9 +464,7 @@ mod tests { let tx = common::test_utils::produce_dummy_empty_transaction(); mempool_handle.push(tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); (sequencer, mempool_handle) } @@ -604,23 +609,21 @@ mod tests { assert!(poll.is_pending()); // Empty the mempool by producing a block - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); // Resolve the pending push assert!(push_fut.await.is_ok()); } #[tokio::test] - async fn produce_new_block_with_mempool_transactions() { + async fn build_block_from_mempool() { let (mut sequencer, mempool_handle) = common_setup().await; let genesis_height = sequencer.chain_height; let tx = common::test_utils::produce_dummy_empty_transaction(); mempool_handle.push(tx).await.unwrap(); - let result = sequencer.produce_new_block_with_mempool_transactions(); + let result = sequencer.build_block_from_mempool(); assert!(result.is_ok()); assert_eq!(sequencer.chain_height, genesis_height + 1); } @@ -645,9 +648,7 @@ mod tests { mempool_handle.push(tx_replay).await.unwrap(); // Create block - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store .get_block_at_id(sequencer.chain_height) @@ -679,9 +680,7 @@ mod tests { // The transaction should be included the first time mempool_handle.push(tx.clone()).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store .get_block_at_id(sequencer.chain_height) @@ -697,9 +696,7 @@ mod tests { // Add same transaction should fail mempool_handle.push(tx.clone()).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store .get_block_at_id(sequencer.chain_height) @@ -738,9 +735,7 @@ mod tests { ); mempool_handle.push(tx.clone()).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store .get_block_at_id(sequencer.chain_height) @@ -778,15 +773,9 @@ mod tests { let config = setup_sequencer_config(); let (mut sequencer, _mempool_handle) = SequencerCoreWithMockClients::start_from_config(config).await; - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); assert_eq!(sequencer.get_pending_blocks().unwrap().len(), 4); } @@ -795,15 +784,9 @@ mod tests { let config = setup_sequencer_config(); let (mut sequencer, _mempool_handle) = SequencerCoreWithMockClients::start_from_config(config).await; - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); + sequencer.produce_new_block().await.unwrap(); let last_finalized_block = 3; sequencer @@ -836,9 +819,7 @@ mod tests { ); mempool_handle.push(tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); // Get the metadata of the last block produced sequencer.store.latest_block_meta().unwrap() @@ -861,9 +842,7 @@ mod tests { mempool_handle.push(tx.clone()).await.unwrap(); // Step 4: Produce new block - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); // Step 5: Verify the new block has correct previous block metadata let new_block = sequencer @@ -876,10 +855,6 @@ mod tests { new_block.header.prev_block_hash, expected_prev_meta.hash, "New block's prev_block_hash should match the stored metadata hash" ); - assert_eq!( - new_block.bedrock_parent_id, expected_prev_meta.msg_id, - "New block's bedrock_parent_id should match the stored metadata msg_id" - ); assert_eq!( new_block.body.transactions, vec![ @@ -914,9 +889,7 @@ mod tests { .await .unwrap(); mempool_handle.push(crafted_clock_tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store @@ -949,15 +922,11 @@ mod tests { // Produce multiple blocks to advance chain height let tx = common::test_utils::produce_dummy_empty_transaction(); mempool_handle.push(tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let tx = common::test_utils::produce_dummy_empty_transaction(); mempool_handle.push(tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); // Return the current chain height (should be genesis_id + 2) sequencer.chain_height @@ -994,9 +963,7 @@ mod tests { ), )); mempool_handle.push(deploy_tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); // Build a user transaction that invokes clock_chain_caller, which in turn chain-calls the // clock program with the clock accounts. The sequencer should detect that the resulting @@ -1021,9 +988,7 @@ mod tests { )); mempool_handle.push(user_tx).await.unwrap(); - sequencer - .produce_new_block_with_mempool_transactions() - .unwrap(); + sequencer.produce_new_block().await.unwrap(); let block = sequencer .store @@ -1057,7 +1022,7 @@ mod tests { mempool_handle.push(tx).await.unwrap(); // Block production must fail because the appended clock tx cannot execute. - let result = sequencer.produce_new_block_with_mempool_transactions(); + let result = sequencer.produce_new_block().await; assert!( result.is_err(), "Block production should abort when clock account data is corrupted" @@ -1076,7 +1041,7 @@ mod tests { program::Program, }; use nssa_core::{ - SharedSecretKey, + InputAccountIdentity, SharedSecretKey, account::AccountWithMetadata, encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, }; @@ -1114,10 +1079,11 @@ mod tests { (&npk, 0), )], Program::serialize_instruction(0_u128).unwrap(), - vec![1], - vec![(npk, 0, shared_secret)], - vec![nsk], - vec![None], + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk: shared_secret, + nsk, + identifier: 0, + }], &Program::authenticated_transfer_program().into(), ) .unwrap(); diff --git a/sequencer/core/src/mock.rs b/sequencer/core/src/mock.rs index 45a682e2..ebe6ea5d 100644 --- a/sequencer/core/src/mock.rs +++ b/sequencer/core/src/mock.rs @@ -1,76 +1,34 @@ -use anyhow::{Result, anyhow}; -use bedrock_client::SignedMantleTx; -use logos_blockchain_core::mantle::ops::channel::ChannelId; +use std::time::Duration; + +use anyhow::Result; +use common::block::Block; use logos_blockchain_key_management_system_service::keys::Ed25519Key; -use url::Url; use crate::{ - block_settlement_client::BlockSettlementClientTrait, config::BedrockConfig, - indexer_client::IndexerClientTrait, + block_publisher::{ + BlockPublisherTrait, CheckpointSink, FinalizedBlockSink, SequencerCheckpoint, + }, + config::BedrockConfig, }; -pub type SequencerCoreWithMockClients = - crate::SequencerCore; +pub type SequencerCoreWithMockClients = crate::SequencerCore; #[derive(Clone)] -pub struct MockBlockSettlementClient { - bedrock_channel_id: ChannelId, - bedrock_signing_key: Ed25519Key, -} +pub struct MockBlockPublisher; -impl BlockSettlementClientTrait for MockBlockSettlementClient { - fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result { - Ok(Self { - bedrock_channel_id: config.channel_id, - bedrock_signing_key: signing_key, - }) +impl BlockPublisherTrait for MockBlockPublisher { + async fn new( + _config: &BedrockConfig, + _bedrock_signing_key: Ed25519Key, + _resubmit_interval: Duration, + _initial_checkpoint: Option, + _on_checkpoint: CheckpointSink, + _on_finalized_block: FinalizedBlockSink, + ) -> Result { + Ok(Self) } - fn bedrock_channel_id(&self) -> ChannelId { - self.bedrock_channel_id - } - - fn bedrock_signing_key(&self) -> &Ed25519Key { - &self.bedrock_signing_key - } - - async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> { + async fn publish_block(&self, _block: &Block) -> Result<()> { Ok(()) } } - -#[derive(Clone)] -pub struct MockBlockSettlementClientWithError { - bedrock_channel_id: ChannelId, - bedrock_signing_key: Ed25519Key, -} - -impl BlockSettlementClientTrait for MockBlockSettlementClientWithError { - fn new(config: &BedrockConfig, signing_key: Ed25519Key) -> Result { - Ok(Self { - bedrock_channel_id: config.channel_id, - bedrock_signing_key: signing_key, - }) - } - - fn bedrock_channel_id(&self) -> ChannelId { - self.bedrock_channel_id - } - - fn bedrock_signing_key(&self) -> &Ed25519Key { - &self.bedrock_signing_key - } - - async fn submit_inscribe_tx_to_bedrock(&self, _tx: SignedMantleTx) -> Result<()> { - Err(anyhow!("Mock error")) - } -} - -#[derive(Copy, Clone)] -pub struct MockIndexerClient; - -impl IndexerClientTrait for MockIndexerClient { - async fn new(_indexer_url: &Url) -> Result { - Ok(Self) - } -} diff --git a/sequencer/service/Cargo.toml b/sequencer/service/Cargo.toml index 6fee808c..beed6be2 100644 --- a/sequencer/service/Cargo.toml +++ b/sequencer/service/Cargo.toml @@ -14,7 +14,6 @@ mempool.workspace = true sequencer_core = { workspace = true, features = ["testnet"] } sequencer_service_protocol.workspace = true sequencer_service_rpc = { workspace = true, features = ["server"] } -indexer_service_rpc = { workspace = true, features = ["client"] } clap = { workspace = true, features = ["derive", "env"] } anyhow.workspace = true diff --git a/sequencer/service/src/lib.rs b/sequencer/service/src/lib.rs index 5373b31f..319b75ad 100644 --- a/sequencer/service/src/lib.rs +++ b/sequencer/service/src/lib.rs @@ -5,15 +5,13 @@ use bytesize::ByteSize; use common::transaction::NSSATransaction; use futures::never::Never; use jsonrpsee::server::ServerHandle; -#[cfg(not(feature = "standalone"))] -use log::warn; use log::{error, info}; use mempool::MemPoolHandle; +#[cfg(not(feature = "standalone"))] +use sequencer_core::SequencerCore; #[cfg(feature = "standalone")] use sequencer_core::SequencerCoreWithMockClients as SequencerCore; pub use sequencer_core::config::*; -#[cfg(not(feature = "standalone"))] -use sequencer_core::{SequencerCore, block_settlement_client::BlockSettlementClientTrait as _}; use sequencer_service_rpc::RpcServer as _; use tokio::{sync::Mutex, task::JoinHandle}; @@ -29,8 +27,6 @@ pub struct SequencerHandle { /// Option because of `Drop` which forbids to simply move out of `self` in `stopped()`. server_handle: Option, main_loop_handle: JoinHandle>, - retry_pending_blocks_loop_handle: JoinHandle>, - listen_for_bedrock_blocks_loop_handle: JoinHandle>, } impl SequencerHandle { @@ -38,15 +34,11 @@ impl SequencerHandle { addr: SocketAddr, server_handle: ServerHandle, main_loop_handle: JoinHandle>, - retry_pending_blocks_loop_handle: JoinHandle>, - listen_for_bedrock_blocks_loop_handle: JoinHandle>, ) -> Self { Self { addr, server_handle: Some(server_handle), main_loop_handle, - retry_pending_blocks_loop_handle, - listen_for_bedrock_blocks_loop_handle, } } @@ -60,8 +52,6 @@ impl SequencerHandle { addr: _, server_handle, main_loop_handle, - retry_pending_blocks_loop_handle, - listen_for_bedrock_blocks_loop_handle, } = &mut self; let server_handle = server_handle.take().expect("Server handle is set"); @@ -75,16 +65,6 @@ impl SequencerHandle { .context("Main loop task panicked")? .context("Main loop exited unexpectedly") } - res = retry_pending_blocks_loop_handle => { - res - .context("Retry pending blocks loop task panicked")? - .context("Retry pending blocks loop exited unexpectedly") - } - res = listen_for_bedrock_blocks_loop_handle => { - res - .context("Listen for bedrock blocks loop task panicked")? - .context("Listen for bedrock blocks loop exited unexpectedly") - } } } @@ -98,14 +78,10 @@ impl SequencerHandle { addr: _, server_handle, main_loop_handle, - retry_pending_blocks_loop_handle, - listen_for_bedrock_blocks_loop_handle, } = self; let stopped = server_handle.as_ref().is_none_or(ServerHandle::is_stopped) - || main_loop_handle.is_finished() - || retry_pending_blocks_loop_handle.is_finished() - || listen_for_bedrock_blocks_loop_handle.is_finished(); + || main_loop_handle.is_finished(); !stopped } @@ -121,13 +97,9 @@ impl Drop for SequencerHandle { addr: _, server_handle, main_loop_handle, - retry_pending_blocks_loop_handle, - listen_for_bedrock_blocks_loop_handle, } = self; main_loop_handle.abort(); - retry_pending_blocks_loop_handle.abort(); - listen_for_bedrock_blocks_loop_handle.abort(); let Some(handle) = server_handle else { return; @@ -141,7 +113,6 @@ impl Drop for SequencerHandle { pub async fn run(config: SequencerConfig, port: u16) -> Result { let block_timeout = config.block_create_timeout; - let retry_pending_blocks_timeout = config.retry_pending_blocks_timeout; let max_block_size = config.max_block_size; let (sequencer_core, mempool_handle) = SequencerCore::start_from_config(config).await; @@ -159,34 +130,10 @@ pub async fn run(config: SequencerConfig, port: u16) -> Result .await?; info!("RPC server started"); - #[cfg(not(feature = "standalone"))] - { - info!("Submitting stored pending blocks"); - retry_pending_blocks(&seq_core_wrapped) - .await - .expect("Failed to submit pending blocks on startup"); - } - info!("Starting main sequencer loop"); - let main_loop_handle = tokio::spawn(main_loop(Arc::clone(&seq_core_wrapped), block_timeout)); + let main_loop_handle = tokio::spawn(main_loop(seq_core_wrapped, block_timeout)); - info!("Starting pending block retry loop"); - let retry_pending_blocks_loop_handle = tokio::spawn(retry_pending_blocks_loop( - Arc::clone(&seq_core_wrapped), - retry_pending_blocks_timeout, - )); - - info!("Starting bedrock block listening loop"); - let listen_for_bedrock_blocks_loop_handle = - tokio::spawn(listen_for_bedrock_blocks_loop(seq_core_wrapped)); - - Ok(SequencerHandle::new( - addr, - server_handle, - main_loop_handle, - retry_pending_blocks_loop_handle, - listen_for_bedrock_blocks_loop_handle, - )) + Ok(SequencerHandle::new(addr, server_handle, main_loop_handle)) } async fn run_server( @@ -235,118 +182,3 @@ async fn main_loop(seq_core: Arc>, block_timeout: Duration) info!("Waiting for new transactions"); } } - -#[cfg(not(feature = "standalone"))] -async fn retry_pending_blocks(seq_core: &Arc>) -> Result<()> { - use std::time::Instant; - - use log::debug; - - let (mut pending_blocks, block_settlement_client) = { - let sequencer_core = seq_core.lock().await; - let client = sequencer_core.block_settlement_client(); - let pending_blocks = sequencer_core - .get_pending_blocks() - .expect("Sequencer should be able to retrieve pending blocks"); - (pending_blocks, client) - }; - - pending_blocks.sort_by(|block1, block2| block1.header.block_id.cmp(&block2.header.block_id)); - - if !pending_blocks.is_empty() { - info!( - "Resubmitting blocks from {} to {}", - pending_blocks.first().unwrap().header.block_id, - pending_blocks.last().unwrap().header.block_id - ); - } - - for block in &pending_blocks { - debug!( - "Resubmitting pending block with id {}", - block.header.block_id - ); - // TODO: We could cache the inscribe tx for each pending block to avoid re-creating it - // on every retry. - let now = Instant::now(); - let (tx, _msg_id) = block_settlement_client - .create_inscribe_tx(block) - .context("Failed to create inscribe tx for pending block")?; - - debug!("Create inscribe: {:?}", now.elapsed()); - - let now = Instant::now(); - if let Err(e) = block_settlement_client - .submit_inscribe_tx_to_bedrock(tx) - .await - { - warn!( - "Failed to resubmit block with id {} with error {e:#}", - block.header.block_id - ); - } - debug!("Post: {:?}", now.elapsed()); - } - Ok(()) -} - -#[cfg(not(feature = "standalone"))] -async fn retry_pending_blocks_loop( - seq_core: Arc>, - retry_pending_blocks_timeout: Duration, -) -> Result { - loop { - tokio::time::sleep(retry_pending_blocks_timeout).await; - retry_pending_blocks(&seq_core).await?; - } -} - -#[cfg(not(feature = "standalone"))] -async fn listen_for_bedrock_blocks_loop(seq_core: Arc>) -> Result { - use indexer_service_rpc::RpcClient as _; - - let indexer_client = seq_core.lock().await.indexer_client(); - - let retry_delay = Duration::from_secs(5); - - loop { - // TODO: Subscribe from the first pending block ID? - let mut subscription = indexer_client - .subscribe_to_finalized_blocks() - .await - .context("Failed to subscribe to finalized blocks")?; - - while let Some(block_id) = subscription.next().await { - let block_id = block_id.context("Failed to get next block from subscription")?; - - info!("Received new L2 block with ID {block_id}"); - - seq_core - .lock() - .await - .clean_finalized_blocks_from_db(block_id) - .with_context(|| { - format!("Failed to clean finalized blocks from DB for block ID {block_id}") - })?; - } - - warn!( - "Block subscription closed unexpectedly, reason: {:?}, retrying after {retry_delay:?}", - subscription.close_reason() - ); - tokio::time::sleep(retry_delay).await; - } -} - -#[cfg(feature = "standalone")] -async fn listen_for_bedrock_blocks_loop(_seq_core: Arc>) -> Result { - std::future::pending::>().await -} - -#[cfg(feature = "standalone")] -async fn retry_pending_blocks_loop( - _seq_core: Arc>, - _retry_pending_blocks_timeout: Duration, -) -> Result { - std::future::pending::>().await -} diff --git a/sequencer/service/src/service.rs b/sequencer/service/src/service.rs index 71645363..0bb8e1dd 100644 --- a/sequencer/service/src/service.rs +++ b/sequencer/service/src/service.rs @@ -8,10 +8,7 @@ use jsonrpsee::{ use log::warn; use mempool::MemPoolHandle; use nssa::{self, program::Program}; -use sequencer_core::{ - DbError, SequencerCore, block_settlement_client::BlockSettlementClientTrait, - indexer_client::IndexerClientTrait, -}; +use sequencer_core::{DbError, SequencerCore, block_publisher::BlockPublisherTrait}; use sequencer_service_protocol::{ Account, AccountId, Block, BlockId, Commitment, HashType, MembershipProof, Nonce, ProgramId, }; @@ -19,15 +16,15 @@ use tokio::sync::Mutex; const NOT_FOUND_ERROR_CODE: i32 = -31999; -pub struct SequencerService { - sequencer: Arc>>, +pub struct SequencerService { + sequencer: Arc>>, mempool_handle: MemPoolHandle, max_block_size: u64, } -impl SequencerService { +impl SequencerService { pub const fn new( - sequencer: Arc>>, + sequencer: Arc>>, mempool_handle: MemPoolHandle, max_block_size: u64, ) -> Self { @@ -40,8 +37,8 @@ impl SequencerService - sequencer_service_rpc::RpcServer for SequencerService +impl sequencer_service_rpc::RpcServer + for SequencerService { async fn send_transaction(&self, tx: NSSATransaction) -> Result { // Reserve ~200 bytes for block header overhead diff --git a/storage/src/indexer/indexer_cells.rs b/storage/src/indexer/indexer_cells.rs index 76a2c035..615902bd 100644 --- a/storage/src/indexer/indexer_cells.rs +++ b/storage/src/indexer/indexer_cells.rs @@ -8,7 +8,8 @@ use crate::{ indexer::{ ACC_NUM_CELL_NAME, BLOCK_HASH_CELL_NAME, BREAKPOINT_CELL_NAME, CF_ACC_META, CF_BREAKPOINT_NAME, CF_HASH_TO_ID, CF_TX_TO_ID, DB_META_LAST_BREAKPOINT_ID, - DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, TX_HASH_CELL_NAME, + DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY, DB_META_ZONE_SDK_INDEXER_CURSOR_KEY, + TX_HASH_CELL_NAME, }, }; @@ -211,6 +212,41 @@ impl SimpleWritableCell for AccNumTxCell { } } +/// Opaque bytes for the zone-sdk indexer cursor `Option<(MsgId, Slot)>`. +/// The caller serializes via `serde_json` (neither type derives borsh). +#[derive(BorshDeserialize)] +pub struct ZoneSdkIndexerCursorCellOwned(pub Vec); + +impl SimpleStorableCell for ZoneSdkIndexerCursorCellOwned { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_ZONE_SDK_INDEXER_CURSOR_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for ZoneSdkIndexerCursorCellOwned {} + +#[derive(BorshSerialize)] +pub struct ZoneSdkIndexerCursorCellRef<'bytes>(pub &'bytes [u8]); + +impl SimpleStorableCell for ZoneSdkIndexerCursorCellRef<'_> { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_ZONE_SDK_INDEXER_CURSOR_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleWritableCell for ZoneSdkIndexerCursorCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize zone-sdk indexer cursor cell".to_owned()), + ) + }) + } +} + #[cfg(test)] mod uniform_tests { use crate::{ diff --git a/storage/src/indexer/mod.rs b/storage/src/indexer/mod.rs index 7ef21258..75538835 100644 --- a/storage/src/indexer/mod.rs +++ b/storage/src/indexer/mod.rs @@ -22,6 +22,8 @@ pub const DB_META_LAST_OBSERVED_L1_LIB_HEADER_ID_IN_DB_KEY: &str = "last_observed_l1_lib_header_in_db"; /// Key base for storing metainformation about the last breakpoint. pub const DB_META_LAST_BREAKPOINT_ID: &str = "last_breakpoint_id"; +/// Key base for storing the zone-sdk indexer cursor (opaque bytes). +pub const DB_META_ZONE_SDK_INDEXER_CURSOR_KEY: &str = "zone_sdk_indexer_cursor"; /// Cell name for a breakpoint. pub const BREAKPOINT_CELL_NAME: &str = "breakpoint"; diff --git a/storage/src/indexer/read_once.rs b/storage/src/indexer/read_once.rs index b1ae0ada..8ab7fd23 100644 --- a/storage/src/indexer/read_once.rs +++ b/storage/src/indexer/read_once.rs @@ -4,7 +4,7 @@ use crate::{ cells::shared_cells::{BlockCell, FirstBlockCell, FirstBlockSetCell, LastBlockCell}, indexer::indexer_cells::{ AccNumTxCell, BlockHashToBlockIdMapCell, BreakpointCellOwned, LastBreakpointIdCell, - LastObservedL1LibHeaderCell, TxHashToBlockIdMapCell, + LastObservedL1LibHeaderCell, TxHashToBlockIdMapCell, ZoneSdkIndexerCursorCellOwned, }, }; @@ -64,4 +64,10 @@ impl RocksDBIO { self.get_opt::(acc_id) .map(|opt| opt.map(|cell| cell.0)) } + + pub fn get_zone_sdk_indexer_cursor_bytes(&self) -> DbResult>> { + Ok(self + .get_opt::(())? + .map(|cell| cell.0)) + } } diff --git a/storage/src/indexer/write_non_atomic.rs b/storage/src/indexer/write_non_atomic.rs index 62b466a2..505360fa 100644 --- a/storage/src/indexer/write_non_atomic.rs +++ b/storage/src/indexer/write_non_atomic.rs @@ -4,6 +4,7 @@ use crate::{ cells::shared_cells::{FirstBlockSetCell, LastBlockCell}, indexer::indexer_cells::{ BreakpointCellRef, LastBreakpointIdCell, LastObservedL1LibHeaderCell, + ZoneSdkIndexerCursorCellRef, }, }; @@ -30,6 +31,10 @@ impl RocksDBIO { self.put(&FirstBlockSetCell(true), ()) } + pub fn put_zone_sdk_indexer_cursor_bytes(&self, bytes: &[u8]) -> DbResult<()> { + self.put(&ZoneSdkIndexerCursorCellRef(bytes), ()) + } + // State pub fn put_breakpoint(&self, br_id: u64, breakpoint: &V03State) -> DbResult<()> { diff --git a/storage/src/sequencer/mod.rs b/storage/src/sequencer/mod.rs index 508f6c29..537d198d 100644 --- a/storage/src/sequencer/mod.rs +++ b/storage/src/sequencer/mod.rs @@ -12,7 +12,7 @@ use crate::{ error::DbError, sequencer::sequencer_cells::{ LastFinalizedBlockIdCell, LatestBlockMetaCellOwned, LatestBlockMetaCellRef, - NSSAStateCellOwned, NSSAStateCellRef, + NSSAStateCellOwned, NSSAStateCellRef, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, }, }; @@ -22,6 +22,8 @@ pub mod sequencer_cells; pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id"; /// Key base for storing metainformation about the latest block meta. pub const DB_META_LATEST_BLOCK_META_KEY: &str = "latest_block_meta"; +/// Key base for storing the zone-sdk sequencer checkpoint (opaque bytes). +pub const DB_META_ZONE_SDK_CHECKPOINT_KEY: &str = "zone_sdk_checkpoint"; /// Key base for storing the NSSA state. pub const DB_NSSA_STATE_KEY: &str = "nssa_state"; @@ -205,6 +207,16 @@ impl RocksDBIO { self.get::(()).map(|val| val.0) } + pub fn get_zone_sdk_checkpoint_bytes(&self) -> DbResult>> { + Ok(self + .get_opt::(())? + .map(|cell| cell.0)) + } + + pub fn put_zone_sdk_checkpoint_bytes(&self, bytes: &[u8]) -> DbResult<()> { + self.put(&ZoneSdkCheckpointCellRef(bytes), ()) + } + pub fn put_block( &self, block: &Block, @@ -275,6 +287,22 @@ impl RocksDBIO { Ok(()) } + /// Mark every pending block with `block_id <= last_finalized` as finalized. + /// Idempotent — already-finalized blocks are skipped. + pub fn clean_pending_blocks_up_to(&self, last_finalized: u64) -> DbResult<()> { + let pending_ids: Vec = self + .get_all_blocks() + .filter_map(Result::ok) + .filter(|b| matches!(b.bedrock_status, BedrockStatus::Pending)) + .map(|b| b.header.block_id) + .filter(|id| *id <= last_finalized) + .collect(); + for id in pending_ids { + self.mark_block_as_finalized(id)?; + } + Ok(()) + } + pub fn mark_block_as_finalized(&self, block_id: u64) -> DbResult<()> { let mut block = self.get_block(block_id)?.ok_or_else(|| { DbError::db_interaction_error(format!("Block with id {block_id} not found")) diff --git a/storage/src/sequencer/sequencer_cells.rs b/storage/src/sequencer/sequencer_cells.rs index 0ad092d7..2bf65367 100644 --- a/storage/src/sequencer/sequencer_cells.rs +++ b/storage/src/sequencer/sequencer_cells.rs @@ -8,7 +8,7 @@ use crate::{ error::DbError, sequencer::{ CF_NSSA_STATE_NAME, DB_META_LAST_FINALIZED_BLOCK_ID, DB_META_LATEST_BLOCK_META_KEY, - DB_NSSA_STATE_KEY, + DB_META_ZONE_SDK_CHECKPOINT_KEY, DB_NSSA_STATE_KEY, }, }; @@ -95,6 +95,42 @@ impl SimpleWritableCell for LatestBlockMetaCellRef<'_> { } } +/// Opaque bytes for the zone-sdk sequencer checkpoint. The caller is +/// responsible for the actual encoding (we use `serde_json` since +/// `SequencerCheckpoint` only derives serde, not borsh). +#[derive(BorshDeserialize)] +pub struct ZoneSdkCheckpointCellOwned(pub Vec); + +impl SimpleStorableCell for ZoneSdkCheckpointCellOwned { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_ZONE_SDK_CHECKPOINT_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for ZoneSdkCheckpointCellOwned {} + +#[derive(BorshSerialize)] +pub struct ZoneSdkCheckpointCellRef<'bytes>(pub &'bytes [u8]); + +impl SimpleStorableCell for ZoneSdkCheckpointCellRef<'_> { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_ZONE_SDK_CHECKPOINT_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleWritableCell for ZoneSdkCheckpointCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize zone-sdk checkpoint cell".to_owned()), + ) + }) + } +} + #[cfg(test)] mod uniform_tests { use crate::{ diff --git a/test_program_methods/guest/src/bin/private_pda_spender.rs b/test_program_methods/guest/src/bin/private_pda_spender.rs new file mode 100644 index 00000000..04ef91a4 --- /dev/null +++ b/test_program_methods/guest/src/bin/private_pda_spender.rs @@ -0,0 +1,118 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, Claim, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; + +/// Single program for group PDA operations. Owns and operates the PDA directly. +/// +/// Instruction: `(pda_seed, noop_program_id, amount, is_deposit)`. +/// Pre-states: `[group_pda, counterparty]`. +/// +/// **Deposit** (`is_deposit = true`, new PDA): +/// Claims PDA via `Claim::Pda(seed)`, increases PDA balance, decreases counterparty. +/// Counterparty must be authorized and owned by this program (or uninitialized). +/// +/// **Spend** (`is_deposit = false`, existing PDA): +/// Decreases PDA balance (this program owns it), increases counterparty. +/// Chains to a noop callee with `pda_seeds` to establish the mask-3 binding +/// that the circuit requires for existing private PDAs. +type Instruction = (PdaSeed, ProgramId, u128, bool); + +#[expect( + clippy::allow_attributes, + reason = "allow is needed because the clones are only redundant in test compilation" +)] +#[allow( + clippy::redundant_clone, + reason = "clones needed in non-test compilation" +)] +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (pda_seed, noop_id, amount, is_deposit), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([pda_pre, counterparty_pre]) = <[_; 2]>::try_from(pre_states.clone()) else { + panic!("expected exactly 2 pre_states: [group_pda, counterparty]"); + }; + + if is_deposit { + // Deposit: claim PDA, transfer balance from counterparty to PDA. + // Both accounts must be owned by this program (or uninitialized) for + // validate_execution to allow balance changes. + assert!( + counterparty_pre.is_authorized, + "Counterparty must be authorized to deposit" + ); + + let mut pda_account = pda_pre.account; + let mut counterparty_account = counterparty_pre.account; + + pda_account.balance = pda_account + .balance + .checked_add(amount) + .expect("PDA balance overflow"); + counterparty_account.balance = counterparty_account + .balance + .checked_sub(amount) + .expect("Counterparty has insufficient balance"); + + let pda_post = AccountPostState::new_claimed_if_default(pda_account, Claim::Pda(pda_seed)); + let counterparty_post = AccountPostState::new(counterparty_account); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + vec![pda_post, counterparty_post], + ) + .write(); + } else { + // Spend: decrease PDA balance (owned by this program), increase counterparty. + // Chain to noop with pda_seeds to establish the mask-3 binding for the + // existing PDA. The noop's pre_states must match our post_states. + // Authorization is enforced by the circuit's binding check, not here. + + let mut pda_account = pda_pre.account.clone(); + let mut counterparty_account = counterparty_pre.account.clone(); + + pda_account.balance = pda_account + .balance + .checked_sub(amount) + .expect("PDA has insufficient balance"); + counterparty_account.balance = counterparty_account + .balance + .checked_add(amount) + .expect("Counterparty balance overflow"); + + let pda_post = AccountPostState::new(pda_account.clone()); + let counterparty_post = AccountPostState::new(counterparty_account.clone()); + + // Chain to noop solely to establish the mask-3 binding via pda_seeds. + let mut noop_pda_pre = pda_pre; + noop_pda_pre.account = pda_account; + noop_pda_pre.is_authorized = true; + + let mut noop_counterparty_pre = counterparty_pre; + noop_counterparty_pre.account = counterparty_account; + + let noop_call = ChainedCall::new(noop_id, vec![noop_pda_pre, noop_counterparty_pre], &()) + .with_pda_seeds(vec![pda_seed]); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + vec![pda_post, counterparty_post], + ) + .with_chained_calls(vec![noop_call]) + .write(); + } +} diff --git a/wallet/src/cli/group.rs b/wallet/src/cli/group.rs new file mode 100644 index 00000000..5cdcc0af --- /dev/null +++ b/wallet/src/cli/group.rs @@ -0,0 +1,295 @@ +use anyhow::{Context as _, Result}; +use clap::Subcommand; +use key_protocol::key_management::group_key_holder::GroupKeyHolder; +use nssa::AccountId; +use nssa_core::program::PdaSeed; + +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, +}; + +/// Group PDA management commands. +#[derive(Subcommand, Debug, Clone)] +pub enum GroupSubcommand { + /// Create a new group with a fresh random GMS. + New { + /// Human-readable name for the group. + name: String, + }, + /// Import a group from raw GMS bytes. + Import { + /// Human-readable name for the group. + name: String, + /// Raw GMS as 64-character hex string. + #[arg(long)] + gms: String, + /// Epoch (defaults to 0). + #[arg(long, default_value = "0")] + epoch: u32, + }, + /// Export the raw GMS hex for backup or manual distribution. + Export { + /// Group name. + name: String, + }, + /// List all groups with their epochs. + #[command(visible_alias = "ls")] + List, + /// Derive keys for a PDA seed and show the resulting AccountId. + Derive { + /// Group name. + name: String, + /// PDA seed as 64-character hex string. + #[arg(long)] + seed: String, + /// Program ID as hex string (u32x8 little-endian). + #[arg(long)] + program_id: String, + }, + /// Remove a group from the wallet. + Remove { + /// Group name. + name: String, + }, + /// Seal the group's GMS for a recipient (invite). + Invite { + /// Group name. + name: String, + /// Recipient's viewing public key as hex string. + #[arg(long)] + vpk: String, + }, + /// Unseal a received GMS and store it (join a group). + Join { + /// Human-readable name to store the group under. + name: String, + /// Sealed GMS as hex string (from the inviter). + #[arg(long)] + sealed: String, + /// Account label or Private/ whose VSK to use for decryption. + #[arg(long)] + account: String, + }, + /// Ratchet the GMS to exclude removed members. + Ratchet { + /// Group name. + name: String, + }, +} + +impl WalletSubcommand for GroupSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + Self::New { name } => { + if wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .is_some() + { + anyhow::bail!("Group '{name}' already exists"); + } + + let holder = GroupKeyHolder::new(); + wallet_core + .storage_mut() + .user_data + .insert_group_key_holder(name.clone(), holder); + wallet_core.store_persistent_data().await?; + + println!("Created group '{name}' at epoch 0"); + Ok(SubcommandReturnValue::Empty) + } + + Self::Import { name, gms, epoch } => { + if wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .is_some() + { + anyhow::bail!("Group '{name}' already exists"); + } + + let gms_bytes: [u8; 32] = hex::decode(&gms) + .context("Invalid GMS hex")? + .try_into() + .map_err(|_| anyhow::anyhow!("GMS must be exactly 32 bytes"))?; + + let holder = GroupKeyHolder::from_gms_and_epoch(gms_bytes, epoch); + wallet_core + .storage_mut() + .user_data + .insert_group_key_holder(name.clone(), holder); + wallet_core.store_persistent_data().await?; + + println!("Imported group '{name}' at epoch {epoch}"); + Ok(SubcommandReturnValue::Empty) + } + + Self::Export { name } => { + let holder = wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .context(format!("Group '{name}' not found"))?; + + let gms_hex = hex::encode(holder.dangerous_raw_gms()); + let epoch = holder.epoch(); + + println!("Group: {name}"); + println!("Epoch: {epoch}"); + println!("GMS: {gms_hex}"); + Ok(SubcommandReturnValue::Empty) + } + + Self::List => { + let holders = &wallet_core.storage().user_data.group_key_holders; + if holders.is_empty() { + println!("No groups found"); + } else { + for (name, holder) in holders { + println!("{name} (epoch {})", holder.epoch()); + } + } + Ok(SubcommandReturnValue::Empty) + } + + Self::Derive { + name, + seed, + program_id, + } => { + let holder = wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .context(format!("Group '{name}' not found"))?; + + let seed_bytes: [u8; 32] = hex::decode(&seed) + .context("Invalid seed hex")? + .try_into() + .map_err(|_| anyhow::anyhow!("Seed must be exactly 32 bytes"))?; + let pda_seed = PdaSeed::new(seed_bytes); + + let pid_bytes = + hex::decode(&program_id).context("Invalid program ID hex")?; + if pid_bytes.len() != 32 { + anyhow::bail!("Program ID must be exactly 32 bytes"); + } + let mut pid: nssa_core::program::ProgramId = [0; 8]; + for (i, chunk) in pid_bytes.chunks_exact(4).enumerate() { + pid[i] = u32::from_le_bytes(chunk.try_into().unwrap()); + } + + let keys = holder.derive_keys_for_pda(&pda_seed); + let npk = keys.generate_nullifier_public_key(); + let vpk = keys.generate_viewing_public_key(); + let account_id = AccountId::for_private_pda(&pid, &pda_seed, &npk); + + println!("Group: {name}"); + println!("NPK: {}", hex::encode(npk.0)); + println!("VPK: {}", hex::encode(&vpk.0)); + println!("AccountId: {account_id}"); + Ok(SubcommandReturnValue::Empty) + } + + Self::Remove { name } => { + if wallet_core + .storage_mut() + .user_data + .group_key_holders + .remove(&name) + .is_none() + { + anyhow::bail!("Group '{name}' not found"); + } + + wallet_core.store_persistent_data().await?; + println!("Removed group '{name}'"); + Ok(SubcommandReturnValue::Empty) + } + + Self::Invite { name, vpk } => { + let holder = wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .context(format!("Group '{name}' not found"))?; + + let vpk_bytes = hex::decode(&vpk).context("Invalid VPK hex")?; + let recipient_vpk = + nssa_core::encryption::shared_key_derivation::Secp256k1Point(vpk_bytes); + + let sealed = holder.seal_for(&recipient_vpk); + println!("{}", hex::encode(&sealed)); + Ok(SubcommandReturnValue::Empty) + } + + Self::Join { + name, + sealed, + account, + } => { + if wallet_core + .storage() + .user_data + .get_group_key_holder(&name) + .is_some() + { + anyhow::bail!("Group '{name}' already exists"); + } + + let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?; + + // Resolve the account to get the VSK + let account_id: nssa::AccountId = account + .parse() + .context("Invalid account ID (use Private/)")?; + let (keychain, _) = wallet_core + .storage() + .user_data + .get_private_account(account_id) + .context("Private account not found")?; + let vsk = keychain.private_key_holder.viewing_secret_key; + + let holder = GroupKeyHolder::unseal(&sealed_bytes, &vsk) + .map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?; + + let epoch = holder.epoch(); + wallet_core + .storage_mut() + .user_data + .insert_group_key_holder(name.clone(), holder); + wallet_core.store_persistent_data().await?; + + println!("Joined group '{name}' at epoch {epoch}"); + Ok(SubcommandReturnValue::Empty) + } + + Self::Ratchet { name } => { + let holder = wallet_core + .storage_mut() + .user_data + .group_key_holders + .get_mut(&name) + .context(format!("Group '{name}' not found"))?; + + let mut salt = [0_u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut salt); + holder.ratchet(salt); + + let epoch = holder.epoch(); + wallet_core.store_persistent_data().await?; + + println!("Ratcheted group '{name}' to epoch {epoch}"); + println!("Re-invite remaining members with 'group invite'"); + Ok(SubcommandReturnValue::Empty) + } + } + } +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 97c4674b..0e487141 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -414,13 +414,7 @@ impl WalletCore { let (output, proof) = nssa::privacy_preserving_transaction::circuit::execute_and_prove( pre_states, instruction_data, - acc_manager.visibility_mask().to_vec(), - private_account_keys - .iter() - .map(|keys| (keys.npk, keys.identifier, keys.ssk)) - .collect::>(), - acc_manager.private_account_auth(), - acc_manager.private_account_membership_proofs(), + acc_manager.account_identities(), &program.to_owned(), ) .unwrap(); diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index d890bcc8..291bb549 100644 --- a/wallet/src/privacy_preserving_tx.rs +++ b/wallet/src/privacy_preserving_tx.rs @@ -2,7 +2,8 @@ use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; use nssa::{AccountId, PrivateKey}; use nssa_core::{ - Identifier, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, + Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, + SharedSecretKey, account::{AccountWithMetadata, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; @@ -51,7 +52,6 @@ impl PrivacyPreservingAccount { pub struct PrivateAccountKeys { pub npk: NullifierPublicKey, - pub identifier: Identifier, pub ssk: SharedSecretKey, pub vpk: ViewingPublicKey, pub epk: EphemeralPublicKey, @@ -67,7 +67,6 @@ enum State { pub struct AccountManager { states: Vec, - visibility_mask: Vec, } impl AccountManager { @@ -75,11 +74,10 @@ impl AccountManager { wallet: &WalletCore, accounts: Vec, ) -> Result { - let mut pre_states = Vec::with_capacity(accounts.len()); - let mut visibility_mask = Vec::with_capacity(accounts.len()); + let mut states = Vec::with_capacity(accounts.len()); for account in accounts { - let (state, mask) = match account { + let state = match account { PrivacyPreservingAccount::Public(account_id) => { let acc = wallet .get_account_public(account_id) @@ -89,13 +87,12 @@ impl AccountManager { let sk = wallet.get_account_public_signing_key(account_id).cloned(); let account = AccountWithMetadata::new(acc.clone(), sk.is_some(), account_id); - (State::Public { account, sk }, 0) + State::Public { account, sk } } PrivacyPreservingAccount::PrivateOwned(account_id) => { - let pre = private_acc_preparation(wallet, account_id).await?; - let mask = if pre.pre_state.is_authorized { 1 } else { 2 }; + let pre = private_acc_preparation(wallet, account_id, false).await?; - (State::Private(pre), mask) + State::Private(pre) } PrivacyPreservingAccount::PrivateForeign { npk, @@ -104,6 +101,9 @@ impl AccountManager { } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, (&npk, identifier)); + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); let pre = AccountPreparedData { nsk: None, npk, @@ -111,13 +111,16 @@ impl AccountManager { vpk, pre_state: auth_acc, proof: None, + ssk, + epk, + is_pda: false, }; - (State::Private(pre), 2) + State::Private(pre) } PrivacyPreservingAccount::PrivatePdaOwned(account_id) => { - let pre = private_acc_preparation(wallet, account_id).await?; - (State::Private(pre), 3) + let pre = private_acc_preparation(wallet, account_id, true).await?; + State::Private(pre) } PrivacyPreservingAccount::PrivatePdaForeign { account_id, @@ -127,6 +130,9 @@ impl AccountManager { } => { let acc = nssa_core::account::Account::default(); let auth_acc = AccountWithMetadata::new(acc, false, account_id); + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); let pre = AccountPreparedData { nsk: None, npk, @@ -134,19 +140,18 @@ impl AccountManager { vpk, pre_state: auth_acc, proof: None, + ssk, + epk, + is_pda: true, }; - (State::Private(pre), 3) + State::Private(pre) } }; - pre_states.push(state); - visibility_mask.push(mask); + states.push(state); } - Ok(Self { - states: pre_states, - visibility_mask, - }) + Ok(Self { states }) } pub fn pre_states(&self) -> Vec { @@ -159,10 +164,6 @@ impl AccountManager { .collect() } - pub fn visibility_mask(&self) -> &[u8] { - &self.visibility_mask - } - pub fn public_account_nonces(&self) -> Vec { self.states .iter() @@ -177,38 +178,63 @@ impl AccountManager { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => { - let eph_holder = EphemeralKeyHolder::new(&pre.npk); + State::Private(pre) => Some(PrivateAccountKeys { + npk: pre.npk, + ssk: pre.ssk, + vpk: pre.vpk.clone(), + epk: pre.epk.clone(), + }), + State::Public { .. } => None, + }) + .collect() + } - Some(PrivateAccountKeys { - npk: pre.npk, - identifier: pre.identifier, - ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), - vpk: pre.vpk.clone(), - epk: eph_holder.generate_ephemeral_public_key(), - }) + /// Build the per-account input vec for the privacy-preserving circuit. Each variant carries + /// exactly the fields the circuit's code path for that account needs, with the ephemeral + /// keys (`ssk`) drawn from the cached values that `private_account_keys` and the message + /// construction also use, so all three views agree on the same ephemeral key. + pub fn account_identities(&self) -> Vec { + self.states + .iter() + .map(|state| match state { + State::Public { .. } => InputAccountIdentity::Public, + State::Private(pre) if pre.is_pda => { + match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => { + InputAccountIdentity::PrivatePdaUpdate { + ssk: pre.ssk, + nsk, + membership_proof, + identifier: pre.identifier, + } + } + _ => InputAccountIdentity::PrivatePdaInit { + npk: pre.npk, + ssk: pre.ssk, + identifier: pre.identifier, + }, + } } - State::Public { .. } => None, - }) - .collect() - } - - pub fn private_account_auth(&self) -> Vec { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => pre.nsk, - State::Public { .. } => None, - }) - .collect() - } - - pub fn private_account_membership_proofs(&self) -> Vec> { - self.states - .iter() - .filter_map(|state| match state { - State::Private(pre) => Some(pre.proof.clone()), - State::Public { .. } => None, + State::Private(pre) => match (pre.nsk, pre.proof.clone()) { + (Some(nsk), Some(membership_proof)) => { + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: pre.ssk, + nsk, + membership_proof, + identifier: pre.identifier, + } + } + (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { + ssk: pre.ssk, + nsk, + identifier: pre.identifier, + }, + (None, _) => InputAccountIdentity::PrivateUnauthorized { + npk: pre.npk, + ssk: pre.ssk, + identifier: pre.identifier, + }, + }, }) .collect() } @@ -241,11 +267,22 @@ struct AccountPreparedData { vpk: ViewingPublicKey, pre_state: AccountWithMetadata, proof: Option, + /// Cached shared-secret key derived once at `AccountManager::new`. Reused for both the + /// circuit input variant (`account_identities()`) and the message ephemeral-key tuples + /// (`private_account_keys()`), so all consumers see the same key. The corresponding + /// `EphemeralKeyHolder` uses `OsRng` and would produce a different value on a second call. + ssk: SharedSecretKey, + /// Cached ephemeral public key, paired with `ssk`. + epk: EphemeralPublicKey, + /// True when this account is a private PDA (owned or foreign). Used by `account_identities()` + /// to select `PrivatePdaInit`/`PrivatePdaUpdate` rather than the standalone private variants. + is_pda: bool, } async fn private_acc_preparation( wallet: &WalletCore, account_id: AccountId, + is_pda: bool, ) -> Result { let Some((from_keys, from_acc, from_identifier)) = wallet.storage.user_data.get_private_account(account_id) @@ -268,6 +305,10 @@ async fn private_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, account_id); + let eph_holder = EphemeralKeyHolder::new(&from_npk); + let ssk = eph_holder.calculate_shared_secret_sender(&from_vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + Ok(AccountPreparedData { nsk: Some(nsk), npk: from_npk, @@ -275,5 +316,9 @@ async fn private_acc_preparation( vpk: from_vpk, pre_state: sender_pre, proof, + ssk, + epk, + is_pda, }) } +