diff --git a/.deny.toml b/.deny.toml index e65cdd34..57b5f759 100644 --- a/.deny.toml +++ b/.deny.toml @@ -14,6 +14,8 @@ ignore = [ { id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." }, { id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" }, { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, + { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, + { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, ] yanked = "deny" unused-ignored-advisory = "deny" 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 73fbb12a..60286441 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]] @@ -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", @@ -3482,10 +3797,16 @@ dependencies = [ name = "indexer_ffi" version = "0.1.0" dependencies = [ + "anyhow", "cbindgen", "indexer_service", + "indexer_service_protocol", + "indexer_service_rpc", + "jsonrpsee", "log", + "nssa", "tokio", + "url", ] [[package]] @@ -3610,7 +3931,9 @@ dependencies = [ "hex", "indexer_ffi", "indexer_service", + "indexer_service_protocol", "indexer_service_rpc", + "jsonrpsee", "key_protocol", "log", "nssa", @@ -3644,6 +3967,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" @@ -3827,7 +4163,7 @@ dependencies = [ "futures-channel", "futures-util", "gloo-net", - "http", + "http 1.4.0", "jsonrpsee-core", "pin-project", "rustls", @@ -3852,7 +4188,7 @@ dependencies = [ "bytes", "futures-timer", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "jsonrpsee-types", @@ -3913,7 +4249,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", @@ -3939,7 +4275,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", @@ -3963,7 +4299,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", @@ -4002,6 +4338,7 @@ dependencies = [ "aes-gcm", "anyhow", "base58", + "bincode", "bip39", "common", "hex", @@ -4327,18 +4664,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]] @@ -4418,8 +5138,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", @@ -4432,8 +5152,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", @@ -4455,8 +5175,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", @@ -4474,8 +5194,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", @@ -4490,11 +5210,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", @@ -4508,7 +5229,6 @@ dependencies = [ "logos-blockchain-time-service", "logos-blockchain-tracing", "logos-blockchain-utils", - "num-bigint 0.4.6", "overwatch", "serde", "serde_with", @@ -4521,8 +5241,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", @@ -4530,16 +5250,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", @@ -4558,8 +5278,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", @@ -4572,6 +5292,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", @@ -4580,6 +5301,7 @@ dependencies = [ "multiaddr", "nom 8.0.0", "num-bigint 0.4.6", + "rpds", "serde", "strum", "thiserror 1.0.69", @@ -4588,10 +5310,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", @@ -4604,11 +5325,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", @@ -4621,8 +5344,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", @@ -4639,8 +5362,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", @@ -4654,8 +5377,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", @@ -4680,8 +5403,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", @@ -4690,8 +5413,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", @@ -4706,8 +5429,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", @@ -4723,8 +5446,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", @@ -4734,6 +5457,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", @@ -4746,17 +5470,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", @@ -4765,8 +5533,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", @@ -4781,8 +5549,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", @@ -4800,8 +5568,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", @@ -4817,8 +5585,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", @@ -4828,8 +5596,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", @@ -4843,8 +5611,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", @@ -4861,15 +5629,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", @@ -4880,8 +5651,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", @@ -4892,6 +5663,7 @@ dependencies = [ "rand 0.8.5", "serde", "tokio", + "tonic", "tracing", "tracing-appender", "tracing-gelf", @@ -4903,8 +5675,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", @@ -4920,8 +5692,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", @@ -4934,16 +5706,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", @@ -4957,6 +5729,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" @@ -4967,6 +5759,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" @@ -5180,7 +5981,7 @@ dependencies = [ "bitflags 2.11.0", "block", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", @@ -5229,6 +6030,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" @@ -5238,7 +6056,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.4.0", "httparse", "memchr", "mime", @@ -5261,7 +6079,8 @@ dependencies = [ "percent-encoding", "serde", "static_assertions", - "unsigned-varint", + "unsigned-varint 0.8.0", + "url", ] [[package]] @@ -5283,24 +6102,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]] @@ -5319,6 +6148,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" @@ -5343,6 +6274,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" @@ -5607,6 +6550,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" @@ -5625,50 +6577,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" @@ -5702,7 +6616,7 @@ checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" dependencies = [ "async-trait", "bytes", - "http", + "http 1.4.0", "opentelemetry", "reqwest", ] @@ -5713,7 +6627,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", @@ -5877,6 +6791,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" @@ -5957,6 +6881,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" @@ -6131,6 +7069,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" @@ -6284,6 +7245,25 @@ dependencies = [ "pyo3-build-config", "quote", "syn 2.0.117", +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]] @@ -6294,12 +7274,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", @@ -6336,7 +7317,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.3", "tracing", "windows-sys 0.59.0", ] @@ -6411,6 +7392,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" @@ -6449,6 +7441,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" @@ -6484,6 +7482,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" @@ -6624,22 +7635,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", @@ -6650,7 +7657,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -6664,6 +7670,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" @@ -7141,6 +8153,21 @@ checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" dependencies = [ "libc", "windows-sys 0.59.0", +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]] @@ -7186,6 +8213,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" @@ -7289,6 +8325,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" @@ -7452,17 +8499,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", @@ -7488,7 +8534,6 @@ dependencies = [ "common", "env_logger", "futures", - "indexer_service_rpc", "jsonrpsee", "log", "mempool", @@ -7719,7 +8764,7 @@ dependencies = [ "const_format", "futures", "gloo-net", - "http", + "http 1.4.0", "http-body-util", "hyper", "inventory", @@ -7871,6 +8916,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" @@ -7890,7 +8945,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures", - "http", + "http 1.4.0", "httparse", "log", "rand 0.8.5", @@ -8060,6 +9115,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" @@ -8113,6 +9179,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" @@ -8190,9 +9262,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", @@ -8204,7 +9276,7 @@ dependencies = [ "etcetera", "ferroid", "futures", - "http", + "http 1.4.0", "itertools 0.14.0", "log", "memchr", @@ -8384,7 +9456,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -8400,27 +9472,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" @@ -8581,7 +9632,7 @@ dependencies = [ "base64 0.22.1", "bytes", "h2", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -8589,7 +9640,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2", + "socket2 0.6.3", "sync_wrapper", "tokio", "tokio-stream", @@ -8639,7 +9690,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "http-range-header", @@ -8847,7 +9898,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http", + "http 1.4.0", "httparse", "log", "rand 0.9.3", @@ -8928,6 +9979,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" @@ -9001,6 +10064,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" @@ -9035,7 +10104,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", ] @@ -9406,6 +10475,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" @@ -9437,6 +10512,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" @@ -9450,6 +10546,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" @@ -9478,6 +10585,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" @@ -9574,6 +10691,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" @@ -9788,6 +10914,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" @@ -9798,6 +10941,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" @@ -9812,7 +10970,7 @@ checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", - "hashlink", + "hashlink 0.10.0", ] [[package]] @@ -9821,6 +10979,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 551c1f98..2cb3aad4 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", "keycard_wallet", "indexer_ffi", @@ -60,6 +59,7 @@ indexer_service_rpc = { path = "indexer/service/rpc" } indexer_ffi = { path = "indexer_ffi" } wallet = { path = "wallet" } wallet-ffi = { path = "wallet-ffi", default-features = false } +indexer_ffi = { path = "indexer/ffi" } clock_core = { path = "programs/clock/core" } token_core = { path = "programs/token/core" } token_program = { path = "programs/token" } @@ -68,7 +68,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" } keycard_wallet = { path = "keycard_wallet" } @@ -125,11 +124,12 @@ schemars = "1.2" async-stream = "0.3.6" pyo3 = { version = "0.24", features = ["auto-initialize"] } -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 36caad85..542fc93f 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 5439d1af..d29bcf02 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 bdbcef61..928963dc 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 d3ca0dab..c2ce2594 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 5e6a011b..a097ad3d 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 57a201c4..abc964b6 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 dd613143..5bb93687 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 6366eba6..615c12eb 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 f9e4d1d4..24fb426c 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 new file mode 100644 index 00000000..fa1306d0 Binary files /dev/null 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 94a90236..59e8e43b 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 58331d6c..d1f22c57 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 2760b7a3..afe2b1c2 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 ff504da1..df4466aa 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 37c9a004..13d81c8f 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 3d69b8cb..12d1ff64 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 873ce66a..9ba00633 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 0846f255..7a5962f9 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 1e285245..544a1718 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/group_pda_spender.bin b/artifacts/test_program_methods/group_pda_spender.bin new file mode 100644 index 00000000..16efb8a4 Binary files /dev/null and b/artifacts/test_program_methods/group_pda_spender.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index cc757683..2034cbe7 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 f152051d..514d3302 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 6d83b95b..45fa2e0b 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 29bcd715..623b25eb 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 c7cc1571..72feec32 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 8f2b1e39..92b61443 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 993c1451..fed326c3 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 579db977..a4fcda58 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 1a541384..53b91181 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/pda_fund_spend_proxy.bin b/artifacts/test_program_methods/pda_fund_spend_proxy.bin new file mode 100644 index 00000000..e377e6bf Binary files /dev/null and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 2b0d979a..69f6b617 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 4b55e871..32dffd53 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..2db36680 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 3bdabade..a9dbb869 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 0aaf1a23..5a4e7036 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 5700322e..db2dc82d 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 600b819d..56a8bd70 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 02ccc149..de524642 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 d239c750..020f0c9b 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/common/src/block.rs b/common/src/block.rs index 3f354c2d..fbc4c9a6 100644 --- a/common/src/block.rs +++ b/common/src/block.rs @@ -114,11 +114,6 @@ impl HashableBlockData { bedrock_parent_id, } } - - #[must_use] - pub fn block_hash(&self) -> BlockHash { - OwnHasher::hash(&borsh::to_vec(&self).unwrap()) - } } impl From for HashableBlockData { 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..bfba5ed2 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. @@ -118,6 +135,12 @@ impl IndexerStore { .get_account_by_id(*account_id)) } + pub fn account_state_at_block(&self, account_id: &AccountId, block_id: u64) -> Result { + Ok(self + .get_state_at_block(block_id)? + .get_account_by_id(*account_id)) + } + pub async fn put_block(&self, mut block: Block, l1_header: HeaderId) -> Result<()> { { let mut state_guard = self.current_state.write().await; @@ -260,4 +283,64 @@ mod tests { assert_eq!(acc1_val.balance, 9920); assert_eq!(acc2_val.balance, 20080); } + + #[tokio::test] + async fn account_state_at_block() { + let home = tempdir().unwrap(); + + let storage = IndexerStore::open_db_with_genesis( + home.as_ref(), + &genesis_block(), + &nssa::V03State::new_with_genesis_accounts( + &[(acc1(), 10000), (acc2(), 20000)], + vec![], + 0, + ), + ) + .unwrap(); + + let mut prev_hash = genesis_block().header.hash; + + let from = acc1(); + let to = acc2(); + let sign_key = acc1_sign_key(); + + for i in 2..10 { + let tx = common::test_utils::create_transaction_native_token_transfer( + from, + i - 2, + to, + 10, + &sign_key, + ); + let block_id = u64::try_from(i).unwrap(); + + let next_block = + common::test_utils::produce_dummy_block(block_id, Some(prev_hash), vec![tx]); + prev_hash = next_block.header.hash; + + storage + .put_block(next_block, HeaderId::from([u8::try_from(i).unwrap(); 32])) + .await + .unwrap(); + } + + // Genesis block: no transfers applied yet. + let acc1_at_1 = storage.account_state_at_block(&acc1(), 1).unwrap(); + let acc2_at_1 = storage.account_state_at_block(&acc2(), 1).unwrap(); + assert_eq!(acc1_at_1.balance, 10000); + assert_eq!(acc2_at_1.balance, 20000); + + // After block 5: 4 transfers of 10 applied (one each in blocks 2..=5). + let acc1_at_5 = storage.account_state_at_block(&acc1(), 5).unwrap(); + let acc2_at_5 = storage.account_state_at_block(&acc2(), 5).unwrap(); + assert_eq!(acc1_at_5.balance, 9960); + assert_eq!(acc2_at_5.balance, 20040); + + // After final block 9: 8 transfers applied; should match current state. + let acc1_at_9 = storage.account_state_at_block(&acc1(), 9).unwrap(); + let acc2_at_9 = storage.account_state_at_block(&acc2(), 9).unwrap(); + assert_eq!(acc1_at_9.balance, 9920); + assert_eq!(acc2_at_9.balance, 20080); + } } 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..eeb31ebb 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 { @@ -63,7 +48,7 @@ impl IndexerCore { .iter() .map(|init_comm_data| { let npk = &init_comm_data.npk; - let account_id = nssa::AccountId::from((npk, 0)); + let account_id = nssa::AccountId::for_regular_private_account(npk, 0); let mut acc = init_comm_data.account.clone(); @@ -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_ffi/Cargo.toml b/indexer/ffi/Cargo.toml similarity index 71% rename from indexer_ffi/Cargo.toml rename to indexer/ffi/Cargo.toml index b55230c6..1e6b1468 100644 --- a/indexer_ffi/Cargo.toml +++ b/indexer/ffi/Cargo.toml @@ -5,9 +5,16 @@ name = "indexer_ffi" version = "0.1.0" [dependencies] +nssa.workspace = true indexer_service.workspace = true +indexer_service_rpc = { workspace = true, features = ["client"] } +indexer_service_protocol.workspace = true + +url.workspace = true log = { workspace = true } tokio = { features = ["rt-multi-thread"], workspace = true } +jsonrpsee.workspace = true +anyhow.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/indexer_ffi/build.rs b/indexer/ffi/build.rs similarity index 100% rename from indexer_ffi/build.rs rename to indexer/ffi/build.rs diff --git a/indexer_ffi/cbindgen.toml b/indexer/ffi/cbindgen.toml similarity index 100% rename from indexer_ffi/cbindgen.toml rename to indexer/ffi/cbindgen.toml diff --git a/indexer/ffi/indexer_ffi.h b/indexer/ffi/indexer_ffi.h new file mode 100644 index 00000000..7626b3b3 --- /dev/null +++ b/indexer/ffi/indexer_ffi.h @@ -0,0 +1,685 @@ +#include +#include +#include +#include + +typedef enum OperationStatus { + Ok = 0, + NullPointer = 1, + InitializationError = 2, + ClientError = 3, +} OperationStatus; + +typedef enum FfiTransactionKind { + Public = 0, + Private, + ProgramDeploy, +} FfiTransactionKind; + +typedef enum FfiBedrockStatus { + Pending = 0, + Safe, + Finalized, +} FfiBedrockStatus; + +typedef struct IndexerServiceFFI { + void *indexer_handle; + void *runtime; + void *indexer_client; +} IndexerServiceFFI; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_IndexerServiceFFI__OperationStatus { + struct IndexerServiceFFI *value; + enum OperationStatus error; +} PointerResult_IndexerServiceFFI__OperationStatus; + +typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_u64__OperationStatus { + uint64_t *value; + enum OperationStatus error; +} PointerResult_u64__OperationStatus; + +typedef uint64_t FfiBlockId; + +/** + * 32-byte array type for `AccountId`, keys, hashes, etc. + */ +typedef struct FfiBytes32 { + uint8_t data[32]; +} FfiBytes32; + +typedef struct FfiBytes32 FfiHashType; + +typedef uint64_t FfiTimestamp; + +/** + * 64-byte array type for signatures, etc. + */ +typedef struct FfiBytes64 { + uint8_t data[64]; +} FfiBytes64; + +typedef struct FfiBytes64 FfiSignature; + +typedef struct FfiBlockHeader { + FfiBlockId block_id; + FfiHashType prev_block_hash; + FfiHashType hash; + FfiTimestamp timestamp; + FfiSignature signature; +} FfiBlockHeader; + +/** + * Program ID - 8 u32 values (32 bytes total). + */ +typedef struct FfiProgramId { + uint32_t data[8]; +} FfiProgramId; + +typedef struct FfiBytes32 FfiAccountId; + +typedef struct FfiVec_FfiAccountId { + FfiAccountId *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiAccountId; + +typedef struct FfiVec_FfiAccountId FfiAccountIdList; + +/** + * U128 - 16 bytes little endian. + */ +typedef struct FfiU128 { + uint8_t data[16]; +} FfiU128; + +typedef struct FfiU128 FfiNonce; + +typedef struct FfiVec_FfiNonce { + FfiNonce *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiNonce; + +typedef struct FfiVec_FfiNonce FfiNonceList; + +typedef struct FfiVec_u32 { + uint32_t *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_u32; + +typedef struct FfiVec_u32 FfiInstructionDataList; + +typedef struct FfiPublicMessage { + struct FfiProgramId program_id; + FfiAccountIdList account_ids; + FfiNonceList nonces; + FfiInstructionDataList instruction_data; +} FfiPublicMessage; + +typedef struct FfiBytes32 FfiPublicKey; + +typedef struct FfiSignaturePubKeyEntry { + FfiSignature signature; + FfiPublicKey public_key; +} FfiSignaturePubKeyEntry; + +typedef struct FfiVec_FfiSignaturePubKeyEntry { + struct FfiSignaturePubKeyEntry *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiSignaturePubKeyEntry; + +typedef struct FfiVec_FfiSignaturePubKeyEntry FfiSignaturePubKeyList; + +typedef struct FfiPublicTransactionBody { + FfiHashType hash; + struct FfiPublicMessage message; + FfiSignaturePubKeyList witness_set; +} FfiPublicTransactionBody; + +/** + * Account data structure - C-compatible version of nssa Account. + * + * Note: `balance` and `nonce` are u128 values represented as little-endian + * byte arrays since C doesn't have native u128 support. + */ +typedef struct FfiAccount { + struct FfiProgramId program_owner; + /** + * Balance as little-endian [u8; 16]. + */ + struct FfiU128 balance; + /** + * Pointer to account data bytes. + */ + uint8_t *data; + /** + * Length of account data. + */ + uintptr_t data_len; + /** + * Capacity of account data. + */ + uintptr_t data_cap; + /** + * Nonce as little-endian [u8; 16]. + */ + struct FfiU128 nonce; +} FfiAccount; + +typedef struct FfiVec_FfiAccount { + struct FfiAccount *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiAccount; + +typedef struct FfiVec_FfiAccount FfiAccountList; + +typedef struct FfiVec_u8 { + uint8_t *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_u8; + +typedef struct FfiVec_u8 FfiVecU8; + +typedef struct FfiEncryptedAccountData { + FfiVecU8 ciphertext; + FfiVecU8 epk; + uint8_t view_tag; +} FfiEncryptedAccountData; + +typedef struct FfiVec_FfiEncryptedAccountData { + struct FfiEncryptedAccountData *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiEncryptedAccountData; + +typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList; + +typedef struct FfiVec_FfiBytes32 { + struct FfiBytes32 *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiBytes32; + +typedef struct FfiVec_FfiBytes32 FfiVecBytes32; + +typedef struct FfiNullifierCommitmentSet { + struct FfiBytes32 nullifier; + struct FfiBytes32 commitment_set_digest; +} FfiNullifierCommitmentSet; + +typedef struct FfiVec_FfiNullifierCommitmentSet { + struct FfiNullifierCommitmentSet *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiNullifierCommitmentSet; + +typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList; + +typedef struct FfiPrivacyPreservingMessage { + FfiAccountIdList public_account_ids; + FfiNonceList nonces; + FfiAccountList public_post_states; + FfiEncryptedAccountDataList encrypted_private_post_states; + FfiVecBytes32 new_commitments; + FfiNullifierCommitmentSetList new_nullifiers; + uint64_t block_validity_window[2]; + uint64_t timestamp_validity_window[2]; +} FfiPrivacyPreservingMessage; + +typedef FfiVecU8 FfiProof; + +typedef struct FfiPrivateTransactionBody { + FfiHashType hash; + struct FfiPrivacyPreservingMessage message; + FfiSignaturePubKeyList witness_set; + FfiProof proof; +} FfiPrivateTransactionBody; + +typedef FfiVecU8 FfiProgramDeploymentMessage; + +typedef struct FfiProgramDeploymentTransactionBody { + FfiHashType hash; + FfiProgramDeploymentMessage message; +} FfiProgramDeploymentTransactionBody; + +typedef struct FfiTransactionBody { + struct FfiPublicTransactionBody *public_body; + struct FfiPrivateTransactionBody *private_body; + struct FfiProgramDeploymentTransactionBody *program_deployment_body; +} FfiTransactionBody; + +typedef struct FfiTransaction { + struct FfiTransactionBody body; + enum FfiTransactionKind kind; +} FfiTransaction; + +typedef struct FfiVec_FfiTransaction { + struct FfiTransaction *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiTransaction; + +typedef struct FfiVec_FfiTransaction FfiBlockBody; + +typedef struct FfiBytes32 FfiMsgId; + +typedef struct FfiBlock { + struct FfiBlockHeader header; + FfiBlockBody body; + enum FfiBedrockStatus bedrock_status; + FfiMsgId bedrock_parent_id; +} FfiBlock; + +typedef struct FfiOption_FfiBlock { + struct FfiBlock *value; + bool is_some; +} FfiOption_FfiBlock; + +typedef struct FfiOption_FfiBlock FfiBlockOpt; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiBlockOpt__OperationStatus { + FfiBlockOpt *value; + enum OperationStatus error; +} PointerResult_FfiBlockOpt__OperationStatus; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiAccount__OperationStatus { + struct FfiAccount *value; + enum OperationStatus error; +} PointerResult_FfiAccount__OperationStatus; + +typedef struct FfiOption_FfiTransaction { + struct FfiTransaction *value; + bool is_some; +} FfiOption_FfiTransaction; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiOption_FfiTransaction_____OperationStatus { + struct FfiOption_FfiTransaction *value; + enum OperationStatus error; +} PointerResult_FfiOption_FfiTransaction_____OperationStatus; + +typedef struct FfiVec_FfiBlock { + struct FfiBlock *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiBlock; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiVec_FfiBlock_____OperationStatus { + struct FfiVec_FfiBlock *value; + enum OperationStatus error; +} PointerResult_FfiVec_FfiBlock_____OperationStatus; + +typedef struct FfiOption_u64 { + uint64_t *value; + bool is_some; +} FfiOption_u64; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { + struct FfiVec_FfiTransaction *value; + enum OperationStatus error; +} PointerResult_FfiVec_FfiTransaction_____OperationStatus; + +/** + * Creates and starts an indexer based on the provided + * configuration file path. + * + * # Arguments + * + * - `config_path`: A pointer to a string representing the path to the configuration file. + * - `port`: Number representing a port, on which indexers RPC will start. + * + * # Returns + * + * An `InitializedIndexerServiceFFIResult` containing either a pointer to the + * initialized `IndexerServiceFFI` or an error code. + */ +InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); + +/** + * Stops and frees the resources associated with the given indexer service. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. + * + * # Returns + * + * An `OperationStatus` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - The `IndexerServiceFFI` instance was created by this library + * - The pointer will not be used after this function returns + */ +enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); + +/** + * # Safety + * It's up to the caller to pass a proper pointer, if somehow from c/c++ side + * this is called with a type which doesn't come from a returned `CString` it + * will cause a segfault. + */ +void free_cstring(char *block); + +/** + * Query the last block id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerServiceFFI *indexer); + +/** + * Query the block by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `block_id`: `u64` number of block id + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer, + FfiBlockId block_id); + +/** + * Query the block by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `hash`: `FfiHashType` - hash of block + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer, + FfiHashType hash); + +/** + * Query the account by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `account_id`: `FfiAccountId` - id of queried account + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer, + FfiAccountId account_id); + +/** + * Query the trasnaction by hash from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `hash`: `FfiHashType` - hash of transaction + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer, + FfiHashType hash); + +/** + * Query the blocks by block range from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `before`: `FfiOption` - end block of query + * - `limit`: `u64` - number of blocks to query before `before` + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer, + struct FfiOption_u64 before, + uint64_t limit); + +/** + * Query the transactions range by account id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `account_id`: `FfiAccountId` - id of queried account + * - `offset`: `u64` - first tx id of query + * - `limit`: `u64` - number of tx ids to query after `offset` + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer, + FfiAccountId account_id, + uint64_t offset, + uint64_t limit); + +/** + * Frees the resources associated with the given ffi account. + * + * # Arguments + * + * - `val`: An instance of `FfiAccount`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiAccount`. + */ +void free_ffi_account(struct FfiAccount val); + +/** + * Frees the resources associated with the given ffi block. + * + * # Arguments + * + * - `val`: An instance of `FfiBlock`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiBlock`. + */ +void free_ffi_block(struct FfiBlock val); + +/** + * Frees the resources associated with the given ffi block option. + * + * # Arguments + * + * - `val`: An instance of `FfiBlockOpt`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiBlockOpt`. + */ +void free_ffi_block_opt(FfiBlockOpt val); + +/** + * Frees the resources associated with the given ffi block vector. + * + * # Arguments + * + * - `val`: An instance of `FfiVec`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiVec`. + */ +void free_ffi_block_vec(struct FfiVec_FfiBlock val); + +/** + * Frees the resources associated with the given ffi transaction. + * + * # Arguments + * + * - `val`: An instance of `FfiTransaction`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiTransaction`. + */ +void free_ffi_transaction(struct FfiTransaction val); + +/** + * Frees the resources associated with the given ffi transaction option. + * + * # Arguments + * + * - `val`: An instance of `FfiOption`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiOption`. + */ +void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); + +/** + * Frees the resources associated with the given vector of ffi transactions. + * + * # Arguments + * + * - `val`: An instance of `FfiVec`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiVec`. + */ +void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val); + +bool is_ok(const enum OperationStatus *self); + +bool is_error(const enum OperationStatus *self); diff --git a/indexer/ffi/src/api/client.rs b/indexer/ffi/src/api/client.rs new file mode 100644 index 00000000..825a57de --- /dev/null +++ b/indexer/ffi/src/api/client.rs @@ -0,0 +1,36 @@ +use std::net::SocketAddr; + +use url::Url; + +use crate::OperationStatus; + +#[derive(Debug, Clone, Copy)] +pub enum UrlProtocol { + Http, + Ws, +} + +impl std::fmt::Display for UrlProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Http => write!(f, "http"), + Self::Ws => write!(f, "ws"), + } + } +} + +pub(crate) 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: + // but clients need to connect to 127.0.0.1: to work reliably + let url_string = if addr.ip().is_unspecified() { + format!("{protocol}://127.0.0.1:{}", addr.port()) + } else { + format!("{protocol}://{addr}") + }; + + url_string.parse().map_err(|e| { + log::error!("Could not parse indexer url: {e}"); + OperationStatus::InitializationError + }) +} diff --git a/indexer_ffi/src/api/lifecycle.rs b/indexer/ffi/src/api/lifecycle.rs similarity index 84% rename from indexer_ffi/src/api/lifecycle.rs rename to indexer/ffi/src/api/lifecycle.rs index 735efd4d..c9cd859d 100644 --- a/indexer_ffi/src/api/lifecycle.rs +++ b/indexer/ffi/src/api/lifecycle.rs @@ -2,7 +2,15 @@ use std::{ffi::c_char, path::PathBuf}; use tokio::runtime::Runtime; -use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus}; +use crate::{ + IndexerServiceFFI, + api::{ + PointerResult, + client::{UrlProtocol, addr_to_url}, + }, + client::{IndexerClient, IndexerClientTrait as _}, + errors::OperationStatus, +}; pub type InitializedIndexerServiceFFIResult = PointerResult; @@ -67,7 +75,13 @@ fn setup_indexer( OperationStatus::InitializationError })?; - Ok(IndexerServiceFFI::new(indexer_handle, rt)) + let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?; + let indexer_client = rt.block_on(IndexerClient::new(&indexer_url)).map_err(|e| { + log::error!("Could not start indexer client: {e}"); + OperationStatus::InitializationError + })?; + + Ok(IndexerServiceFFI::new(indexer_handle, rt, indexer_client)) } /// Stops and frees the resources associated with the given indexer service. diff --git a/indexer_ffi/src/api/memory.rs b/indexer/ffi/src/api/memory.rs similarity index 100% rename from indexer_ffi/src/api/memory.rs rename to indexer/ffi/src/api/memory.rs diff --git a/indexer_ffi/src/api/mod.rs b/indexer/ffi/src/api/mod.rs similarity index 64% rename from indexer_ffi/src/api/mod.rs rename to indexer/ffi/src/api/mod.rs index e84a3913..ea2b91d7 100644 --- a/indexer_ffi/src/api/mod.rs +++ b/indexer/ffi/src/api/mod.rs @@ -1,5 +1,8 @@ pub use result::PointerResult; +pub mod client; pub mod lifecycle; pub mod memory; +pub mod query; pub mod result; +pub mod types; diff --git a/indexer/ffi/src/api/query.rs b/indexer/ffi/src/api/query.rs new file mode 100644 index 00000000..1e39d961 --- /dev/null +++ b/indexer/ffi/src/api/query.rs @@ -0,0 +1,334 @@ +use indexer_service_protocol::{AccountId, HashType}; +use indexer_service_rpc::RpcClient as _; + +use crate::{ + IndexerServiceFFI, + api::{ + PointerResult, + types::{ + FfiAccountId, FfiBlockId, FfiHashType, FfiOption, FfiVec, + account::FfiAccount, + block::{FfiBlock, FfiBlockOpt}, + transaction::FfiTransaction, + }, + }, + errors::OperationStatus, +}; + +/// Query the last block id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_last_block( + indexer: *const IndexerServiceFFI, +) -> PointerResult { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_last_finalized_block_id()) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + PointerResult::from_value, + ) +} + +/// Query the block by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `block_id`: `u64` number of block id +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block( + indexer: *const IndexerServiceFFI, + block_id: FfiBlockId, +) -> PointerResult { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_block_by_id(block_id)) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + FfiBlockOpt::from_value(block.into()) + }); + + PointerResult::from_value(block_ffi) + }, + ) +} + +/// Query the block by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `hash`: `FfiHashType` - hash of block +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block_by_hash( + indexer: *const IndexerServiceFFI, + hash: FfiHashType, +) -> PointerResult { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_block_by_hash(HashType(hash.data))) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + FfiBlockOpt::from_value(block.into()) + }); + + PointerResult::from_value(block_ffi) + }, + ) +} + +/// Query the account by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `account_id`: `FfiAccountId` - id of queried account +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, +) -> PointerResult { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_account(AccountId { + value: account_id.data, + })) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |acc| { + let acc_nssa: nssa::Account = + acc.try_into().expect("Source is in blocks, must fit"); + PointerResult::from_value(acc_nssa.into()) + }, + ) +} + +/// Query the trasnaction by hash from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `hash`: `FfiHashType` - hash of transaction +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_transaction( + indexer: *const IndexerServiceFFI, + hash: FfiHashType, +) -> PointerResult, OperationStatus> { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_transaction(HashType(hash.data))) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |tx_opt| { + let tx_ffi = tx_opt.map_or_else(FfiOption::::from_none, |tx| { + FfiOption::::from_value(tx.into()) + }); + + PointerResult::from_value(tx_ffi) + }, + ) +} + +/// Query the blocks by block range from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `before`: `FfiOption` - end block of query +/// - `limit`: `u64` - number of blocks to query before `before` +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block_vec( + indexer: *const IndexerServiceFFI, + before: FfiOption, + limit: u64, +) -> PointerResult, OperationStatus> { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + let before_std = before.is_some.then(|| unsafe { *before.value }); + + runtime + .block_on(client.get_blocks(before_std, limit)) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_vec| { + PointerResult::from_value( + block_vec + .into_iter() + .map(Into::into) + .collect::>() + .into(), + ) + }, + ) +} + +/// Query the transactions range by account id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `account_id`: `FfiAccountId` - id of queried account +/// - `offset`: `u64` - first tx id of query +/// - `limit`: `u64` - number of tx ids to query after `offset` +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_transactions_by_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, + offset: u64, + limit: u64, +) -> PointerResult, OperationStatus> { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + + let client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_transactions_by_account( + AccountId { + value: account_id.data, + }, + offset, + limit, + )) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |tx_vec| { + PointerResult::from_value( + tx_vec + .into_iter() + .map(Into::into) + .collect::>() + .into(), + ) + }, + ) +} diff --git a/indexer_ffi/src/api/result.rs b/indexer/ffi/src/api/result.rs similarity index 100% rename from indexer_ffi/src/api/result.rs rename to indexer/ffi/src/api/result.rs diff --git a/indexer/ffi/src/api/types/account.rs b/indexer/ffi/src/api/types/account.rs new file mode 100644 index 00000000..6c35347f --- /dev/null +++ b/indexer/ffi/src/api/types/account.rs @@ -0,0 +1,119 @@ +use indexer_service_protocol::ProgramId; + +use crate::api::types::{FfiBytes32, FfiProgramId, FfiU128}; + +/// Account data structure - C-compatible version of nssa Account. +/// +/// Note: `balance` and `nonce` are u128 values represented as little-endian +/// byte arrays since C doesn't have native u128 support. +#[repr(C)] +pub struct FfiAccount { + pub program_owner: FfiProgramId, + /// Balance as little-endian [u8; 16]. + pub balance: FfiU128, + /// Pointer to account data bytes. + pub data: *mut u8, + /// Length of account data. + pub data_len: usize, + /// Capacity of account data. + pub data_cap: usize, + /// Nonce as little-endian [u8; 16]. + pub nonce: FfiU128, +} + +// Helper functions to convert between Rust and FFI types + +impl From<&nssa::AccountId> for FfiBytes32 { + fn from(id: &nssa::AccountId) -> Self { + Self::from_account_id(id) + } +} + +impl From for FfiAccount { + fn from(value: nssa::Account) -> Self { + let nssa::Account { + program_owner, + balance, + data, + nonce, + } = value; + + let (data, data_len, data_cap) = data.into_inner().into_raw_parts(); + + let program_owner = FfiProgramId { + data: program_owner, + }; + Self { + program_owner, + balance: balance.into(), + data, + data_len, + data_cap, + nonce: nonce.0.into(), + } + } +} + +impl From for indexer_service_protocol::Account { + fn from(value: FfiAccount) -> Self { + let FfiAccount { + program_owner, + balance, + data, + data_cap, + data_len, + nonce, + } = value; + + Self { + program_owner: ProgramId(program_owner.data), + balance: balance.into(), + data: indexer_service_protocol::Data(unsafe { + Vec::from_raw_parts(data, data_len, data_cap) + }), + nonce: nonce.into(), + } + } +} + +impl From<&FfiAccount> for indexer_service_protocol::Account { + fn from(value: &FfiAccount) -> Self { + let &FfiAccount { + program_owner, + balance, + data, + data_cap, + data_len, + nonce, + } = value; + + Self { + program_owner: ProgramId(program_owner.data), + balance: balance.into(), + data: indexer_service_protocol::Data(unsafe { + Vec::from_raw_parts(data, data_len, data_cap) + }), + nonce: nonce.into(), + } + } +} + +/// Frees the resources associated with the given ffi account. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiAccount`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiAccount`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) { + let orig_val: indexer_service_protocol::Account = val.into(); + drop(orig_val); +} diff --git a/indexer/ffi/src/api/types/block.rs b/indexer/ffi/src/api/types/block.rs new file mode 100644 index 00000000..bca2fdb5 --- /dev/null +++ b/indexer/ffi/src/api/types/block.rs @@ -0,0 +1,199 @@ +use indexer_service_protocol::{ + BedrockStatus, Block, BlockHeader, HashType, MantleMsgId, Signature, +}; + +use crate::api::types::{ + FfiBlockId, FfiHashType, FfiMsgId, FfiOption, FfiSignature, FfiTimestamp, FfiVec, + transaction::free_ffi_transaction_vec, vectors::FfiBlockBody, +}; + +#[repr(C)] +pub struct FfiBlock { + pub header: FfiBlockHeader, + pub body: FfiBlockBody, + pub bedrock_status: FfiBedrockStatus, + pub bedrock_parent_id: FfiMsgId, +} + +impl From for FfiBlock { + fn from(value: Block) -> Self { + let Block { + header, + body, + bedrock_status, + bedrock_parent_id, + } = value; + + Self { + header: header.into(), + body: body + .transactions + .into_iter() + .map(Into::into) + .collect::>() + .into(), + bedrock_status: bedrock_status.into(), + bedrock_parent_id: bedrock_parent_id.into(), + } + } +} + +pub type FfiBlockOpt = FfiOption; + +#[repr(C)] +pub struct FfiBlockHeader { + pub block_id: FfiBlockId, + pub prev_block_hash: FfiHashType, + pub hash: FfiHashType, + pub timestamp: FfiTimestamp, + pub signature: FfiSignature, +} + +impl From for FfiBlockHeader { + fn from(value: BlockHeader) -> Self { + let BlockHeader { + block_id, + prev_block_hash, + hash, + timestamp, + signature, + } = value; + + Self { + block_id, + prev_block_hash: prev_block_hash.into(), + hash: hash.into(), + timestamp, + signature: signature.into(), + } + } +} + +#[repr(C)] +pub enum FfiBedrockStatus { + Pending = 0x0, + Safe, + Finalized, +} + +impl From for FfiBedrockStatus { + fn from(value: BedrockStatus) -> Self { + match value { + BedrockStatus::Finalized => Self::Finalized, + BedrockStatus::Pending => Self::Pending, + BedrockStatus::Safe => Self::Safe, + } + } +} + +impl From for BedrockStatus { + fn from(value: FfiBedrockStatus) -> Self { + match value { + FfiBedrockStatus::Finalized => Self::Finalized, + FfiBedrockStatus::Pending => Self::Pending, + FfiBedrockStatus::Safe => Self::Safe, + } + } +} + +/// Frees the resources associated with the given ffi block. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiBlock`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiBlock`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { + // We don't really need all the casts, but just in case + // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop + let _ = BlockHeader { + block_id: val.header.block_id, + prev_block_hash: HashType(val.header.prev_block_hash.data), + hash: HashType(val.header.hash.data), + timestamp: val.header.timestamp, + signature: Signature(val.header.signature.data), + }; + let ffi_tx_ffi_vec = val.body; + + #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] + let _: BedrockStatus = val.bedrock_status.into(); + + let _ = MantleMsgId(val.bedrock_parent_id.data); + + unsafe { + free_ffi_transaction_vec(ffi_tx_ffi_vec); + }; +} + +/// Frees the resources associated with the given ffi block option. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiBlockOpt`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiBlockOpt`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { + if val.is_some { + let value = unsafe { Box::from_raw(val.value) }; + + // We don't really need all the casts, but just in case + // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop + let _ = BlockHeader { + block_id: value.header.block_id, + prev_block_hash: HashType(value.header.prev_block_hash.data), + hash: HashType(value.header.hash.data), + timestamp: value.header.timestamp, + signature: Signature(value.header.signature.data), + }; + let ffi_tx_ffi_vec = value.body; + + #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] + let _: BedrockStatus = value.bedrock_status.into(); + + let _ = MantleMsgId(value.bedrock_parent_id.data); + + unsafe { + free_ffi_transaction_vec(ffi_tx_ffi_vec); + }; + } +} + +/// Frees the resources associated with the given ffi block vector. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiVec`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiVec`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec) { + let ffi_block_std_vec: Vec<_> = val.into(); + for block in ffi_block_std_vec { + unsafe { + free_ffi_block(block); + } + } +} diff --git a/indexer/ffi/src/api/types/mod.rs b/indexer/ffi/src/api/types/mod.rs new file mode 100644 index 00000000..2e7a77ad --- /dev/null +++ b/indexer/ffi/src/api/types/mod.rs @@ -0,0 +1,165 @@ +use indexer_service_protocol::{AccountId, HashType, MantleMsgId, ProgramId, PublicKey, Signature}; + +pub mod account; +pub mod block; +pub mod transaction; +pub mod vectors; + +/// 32-byte array type for `AccountId`, keys, hashes, etc. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiBytes32 { + pub data: [u8; 32], +} + +/// 64-byte array type for signatures, etc. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FfiBytes64 { + pub data: [u8; 64], +} + +/// Program ID - 8 u32 values (32 bytes total). +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiProgramId { + pub data: [u32; 8], +} + +impl From for FfiProgramId { + fn from(value: ProgramId) -> Self { + Self { data: value.0 } + } +} + +/// U128 - 16 bytes little endian. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiU128 { + pub data: [u8; 16], +} + +impl FfiBytes32 { + /// Create from a 32-byte array. + #[must_use] + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self { data: bytes } + } + + /// Create from an `AccountId`. + #[must_use] + pub const fn from_account_id(id: &nssa::AccountId) -> Self { + Self { data: *id.value() } + } +} + +impl From for FfiU128 { + fn from(value: u128) -> Self { + Self { + data: value.to_le_bytes(), + } + } +} + +impl From for u128 { + fn from(value: FfiU128) -> Self { + Self::from_le_bytes(value.data) + } +} + +pub type FfiHashType = FfiBytes32; +pub type FfiMsgId = FfiBytes32; +pub type FfiBlockId = u64; +pub type FfiTimestamp = u64; +pub type FfiSignature = FfiBytes64; +pub type FfiAccountId = FfiBytes32; +pub type FfiNonce = FfiU128; +pub type FfiPublicKey = FfiBytes32; + +impl From for FfiHashType { + fn from(value: HashType) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiMsgId { + fn from(value: MantleMsgId) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiSignature { + fn from(value: Signature) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiAccountId { + fn from(value: AccountId) -> Self { + Self { data: value.value } + } +} + +impl From for FfiPublicKey { + fn from(value: PublicKey) -> Self { + Self { data: value.0 } + } +} + +#[repr(C)] +pub struct FfiVec { + pub entries: *mut T, + pub len: usize, + pub capacity: usize, +} + +impl From> for FfiVec { + fn from(value: Vec) -> Self { + let (entries, len, capacity) = value.into_raw_parts(); + Self { + entries, + len, + capacity, + } + } +} + +impl From> for Vec { + fn from(value: FfiVec) -> Self { + unsafe { Self::from_raw_parts(value.entries, value.len, value.capacity) } + } +} + +impl FfiVec { + /// # Safety + /// + /// `index` must be lesser than `self.len`. + #[must_use] + pub unsafe fn get(&self, index: usize) -> &T { + let ptr = unsafe { self.entries.add(index) }; + unsafe { &*ptr } + } +} + +#[repr(C)] +pub struct FfiOption { + pub value: *mut T, + pub is_some: bool, +} + +impl FfiOption { + pub fn from_value(val: T) -> Self { + Self { + value: Box::into_raw(Box::new(val)), + is_some: true, + } + } + + #[must_use] + pub const fn from_none() -> Self { + Self { + value: std::ptr::null_mut(), + is_some: false, + } + } +} diff --git a/indexer/ffi/src/api/types/transaction.rs b/indexer/ffi/src/api/types/transaction.rs new file mode 100644 index 00000000..ee3bd01b --- /dev/null +++ b/indexer/ffi/src/api/types/transaction.rs @@ -0,0 +1,548 @@ +use indexer_service_protocol::{ + AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData, + EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage, + PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, + ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, + ValidityWindow, WitnessSet, +}; + +use crate::api::types::{ + FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec, + vectors::{ + FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList, + FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof, + FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8, + }, +}; + +#[repr(C)] +pub struct FfiPublicTransactionBody { + pub hash: FfiHashType, + pub message: FfiPublicMessage, + pub witness_set: FfiSignaturePubKeyList, +} + +impl From for FfiPublicTransactionBody { + fn from(value: PublicTransaction) -> Self { + let PublicTransaction { + hash, + message, + witness_set, + } = value; + + Self { + hash: hash.into(), + message: message.into(), + witness_set: witness_set + .signatures_and_public_keys + .into_iter() + .map(Into::into) + .collect::>() + .into(), + } + } +} + +impl From> for PublicTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: PublicMessage { + program_id: ProgramId(value.message.program_id.data), + account_ids: { + let std_vec: Vec<_> = value.message.account_ids.into(); + std_vec + .into_iter() + .map(|ffi_val| AccountId { + value: ffi_val.data, + }) + .collect() + }, + nonces: { + let std_vec: Vec<_> = value.message.nonces.into(); + std_vec.into_iter().map(Into::into).collect() + }, + instruction_data: value.message.instruction_data.into(), + }, + witness_set: WitnessSet { + signatures_and_public_keys: { + let std_vec: Vec<_> = value.witness_set.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Signature(ffi_val.signature.data), + PublicKey(ffi_val.public_key.data), + ) + }) + .collect() + }, + proof: None, + }, + } + } +} + +#[repr(C)] +pub struct FfiPublicMessage { + pub program_id: FfiProgramId, + pub account_ids: FfiAccountIdList, + pub nonces: FfiNonceList, + pub instruction_data: FfiInstructionDataList, +} + +impl From for FfiPublicMessage { + fn from(value: PublicMessage) -> Self { + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = value; + + Self { + program_id: program_id.into(), + account_ids: account_ids + .into_iter() + .map(Into::into) + .collect::>() + .into(), + nonces: nonces + .into_iter() + .map(Into::into) + .collect::>() + .into(), + instruction_data: instruction_data.into(), + } + } +} + +#[repr(C)] +pub struct FfiPrivateTransactionBody { + pub hash: FfiHashType, + pub message: FfiPrivacyPreservingMessage, + pub witness_set: FfiSignaturePubKeyList, + pub proof: FfiProof, +} + +impl From for FfiPrivateTransactionBody { + fn from(value: PrivacyPreservingTransaction) -> Self { + let PrivacyPreservingTransaction { + hash, + message, + witness_set, + } = value; + + Self { + hash: hash.into(), + message: message.into(), + witness_set: witness_set + .signatures_and_public_keys + .into_iter() + .map(Into::into) + .collect::>() + .into(), + proof: witness_set + .proof + .expect("Private execution: proof must be present") + .0 + .into(), + } + } +} + +impl From> for PrivacyPreservingTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: PrivacyPreservingMessage { + public_account_ids: { + let std_vec: Vec<_> = value.message.public_account_ids.into(); + std_vec + .into_iter() + .map(|ffi_val| AccountId { + value: ffi_val.data, + }) + .collect() + }, + nonces: { + let std_vec: Vec<_> = value.message.nonces.into(); + std_vec.into_iter().map(Into::into).collect() + }, + public_post_states: { + let std_vec: Vec<_> = value.message.public_post_states.into(); + std_vec.into_iter().map(Into::into).collect() + }, + encrypted_private_post_states: { + let std_vec: Vec<_> = value.message.encrypted_private_post_states.into(); + std_vec + .into_iter() + .map(|ffi_val| EncryptedAccountData { + ciphertext: Ciphertext(ffi_val.ciphertext.into()), + epk: EphemeralPublicKey(ffi_val.epk.into()), + view_tag: ffi_val.view_tag, + }) + .collect() + }, + new_commitments: { + let std_vec: Vec<_> = value.message.new_commitments.into(); + std_vec + .into_iter() + .map(|ffi_val| Commitment(ffi_val.data)) + .collect() + }, + new_nullifiers: { + let std_vec: Vec<_> = value.message.new_nullifiers.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Nullifier(ffi_val.nullifier.data), + CommitmentSetDigest(ffi_val.commitment_set_digest.data), + ) + }) + .collect() + }, + block_validity_window: cast_ffi_validity_window( + value.message.block_validity_window, + ), + timestamp_validity_window: cast_ffi_validity_window( + value.message.timestamp_validity_window, + ), + }, + witness_set: WitnessSet { + signatures_and_public_keys: { + let std_vec: Vec<_> = value.witness_set.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Signature(ffi_val.signature.data), + PublicKey(ffi_val.public_key.data), + ) + }) + .collect() + }, + proof: Some(Proof(value.proof.into())), + }, + } + } +} + +#[repr(C)] +pub struct FfiPrivacyPreservingMessage { + pub public_account_ids: FfiAccountIdList, + pub nonces: FfiNonceList, + pub public_post_states: FfiAccountList, + pub encrypted_private_post_states: FfiEncryptedAccountDataList, + pub new_commitments: FfiVecBytes32, + pub new_nullifiers: FfiNullifierCommitmentSetList, + pub block_validity_window: [u64; 2], + pub timestamp_validity_window: [u64; 2], +} + +impl From for FfiPrivacyPreservingMessage { + fn from(value: PrivacyPreservingMessage) -> Self { + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + block_validity_window, + timestamp_validity_window, + } = value; + + Self { + public_account_ids: public_account_ids + .into_iter() + .map(Into::into) + .collect::>() + .into(), + nonces: nonces + .into_iter() + .map(Into::into) + .collect::>() + .into(), + public_post_states: public_post_states + .into_iter() + .map(|acc_ind| -> nssa::Account { + acc_ind.try_into().expect("Source is in blocks, must fit") + }) + .map(Into::into) + .collect::>() + .into(), + encrypted_private_post_states: encrypted_private_post_states + .into_iter() + .map(Into::into) + .collect::>() + .into(), + new_commitments: new_commitments + .into_iter() + .map(|comm| FfiBytes32 { data: comm.0 }) + .collect::>() + .into(), + new_nullifiers: new_nullifiers + .into_iter() + .map(Into::into) + .collect::>() + .into(), + block_validity_window: cast_validity_window(block_validity_window), + timestamp_validity_window: cast_validity_window(timestamp_validity_window), + } + } +} + +#[repr(C)] +pub struct FfiNullifierCommitmentSet { + pub nullifier: FfiBytes32, + pub commitment_set_digest: FfiBytes32, +} + +impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet { + fn from(value: (Nullifier, CommitmentSetDigest)) -> Self { + Self { + nullifier: FfiBytes32 { data: value.0.0 }, + commitment_set_digest: FfiBytes32 { data: value.1.0 }, + } + } +} + +#[repr(C)] +pub struct FfiEncryptedAccountData { + pub ciphertext: FfiVecU8, + pub epk: FfiVecU8, + pub view_tag: u8, +} + +impl From for FfiEncryptedAccountData { + fn from(value: EncryptedAccountData) -> Self { + let EncryptedAccountData { + ciphertext, + epk, + view_tag, + } = value; + + Self { + ciphertext: ciphertext.0.into(), + epk: epk.0.into(), + view_tag, + } + } +} + +#[repr(C)] +pub struct FfiSignaturePubKeyEntry { + pub signature: FfiSignature, + pub public_key: FfiPublicKey, +} + +impl From<(Signature, PublicKey)> for FfiSignaturePubKeyEntry { + fn from(value: (Signature, PublicKey)) -> Self { + Self { + signature: value.0.into(), + public_key: value.1.into(), + } + } +} + +#[repr(C)] +pub struct FfiProgramDeploymentTransactionBody { + pub hash: FfiHashType, + pub message: FfiProgramDeploymentMessage, +} + +impl From> for ProgramDeploymentTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: ProgramDeploymentMessage { + bytecode: value.message.into(), + }, + } + } +} + +impl From for FfiProgramDeploymentTransactionBody { + fn from(value: ProgramDeploymentTransaction) -> Self { + let ProgramDeploymentTransaction { hash, message } = value; + + Self { + hash: hash.into(), + message: message.bytecode.into(), + } + } +} + +#[repr(C)] +pub struct FfiTransactionBody { + pub public_body: *mut FfiPublicTransactionBody, + pub private_body: *mut FfiPrivateTransactionBody, + pub program_deployment_body: *mut FfiProgramDeploymentTransactionBody, +} + +#[repr(C)] +pub struct FfiTransaction { + pub body: FfiTransactionBody, + pub kind: FfiTransactionKind, +} + +impl From for FfiTransaction { + fn from(value: Transaction) -> Self { + match value { + Transaction::Public(pub_tx) => Self { + body: FfiTransactionBody { + public_body: Box::into_raw(Box::new(pub_tx.into())), + private_body: std::ptr::null_mut(), + program_deployment_body: std::ptr::null_mut(), + }, + kind: FfiTransactionKind::Public, + }, + Transaction::PrivacyPreserving(priv_tx) => Self { + body: FfiTransactionBody { + public_body: std::ptr::null_mut(), + private_body: Box::into_raw(Box::new(priv_tx.into())), + program_deployment_body: std::ptr::null_mut(), + }, + kind: FfiTransactionKind::Private, + }, + Transaction::ProgramDeployment(pr_dep_tx) => Self { + body: FfiTransactionBody { + public_body: std::ptr::null_mut(), + private_body: std::ptr::null_mut(), + program_deployment_body: Box::into_raw(Box::new(pr_dep_tx.into())), + }, + kind: FfiTransactionKind::ProgramDeploy, + }, + } + } +} + +#[repr(C)] +pub enum FfiTransactionKind { + Public = 0x0, + Private, + ProgramDeploy, +} + +/// Frees the resources associated with the given ffi transaction. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiTransaction`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiTransaction`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { + match val.kind { + FfiTransactionKind::Public => { + let body = unsafe { Box::from_raw(val.body.public_body) }; + let std_body: PublicTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::Private => { + let body = unsafe { Box::from_raw(val.body.private_body) }; + let std_body: PrivacyPreservingTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::ProgramDeploy => { + let body = unsafe { Box::from_raw(val.body.program_deployment_body) }; + let std_body: ProgramDeploymentTransaction = body.into(); + drop(std_body); + } + } +} + +/// Frees the resources associated with the given ffi transaction option. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiOption`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiOption`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption) { + if val.is_some { + let value = unsafe { Box::from_raw(val.value) }; + + match value.kind { + FfiTransactionKind::Public => { + let body = unsafe { Box::from_raw(value.body.public_body) }; + let std_body: PublicTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::Private => { + let body = unsafe { Box::from_raw(value.body.private_body) }; + let std_body: PrivacyPreservingTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::ProgramDeploy => { + let body = unsafe { Box::from_raw(value.body.program_deployment_body) }; + let std_body: ProgramDeploymentTransaction = body.into(); + drop(std_body); + } + } + } +} + +/// Frees the resources associated with the given vector of ffi transactions. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiVec`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiVec`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { + let ffi_tx_std_vec: Vec<_> = val.into(); + for tx in ffi_tx_std_vec { + unsafe { + free_ffi_transaction(tx); + } + } +} + +fn cast_validity_window(window: ValidityWindow) -> [u64; 2] { + [ + window.0.0.unwrap_or_default(), + window.0.1.unwrap_or(u64::MAX), + ] +} + +const fn cast_ffi_validity_window(ffi_window: [u64; 2]) -> ValidityWindow { + let left = if ffi_window[0] == 0 { + None + } else { + Some(ffi_window[0]) + }; + + let right = if ffi_window[1] == u64::MAX { + None + } else { + Some(ffi_window[1]) + }; + + ValidityWindow((left, right)) +} diff --git a/indexer/ffi/src/api/types/vectors.rs b/indexer/ffi/src/api/types/vectors.rs new file mode 100644 index 00000000..46f08737 --- /dev/null +++ b/indexer/ffi/src/api/types/vectors.rs @@ -0,0 +1,31 @@ +use crate::api::types::{ + FfiAccountId, FfiBytes32, FfiNonce, FfiVec, + account::FfiAccount, + transaction::{ + FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction, + }, +}; + +pub type FfiVecU8 = FfiVec; + +pub type FfiAccountList = FfiVec; + +pub type FfiAccountIdList = FfiVec; + +pub type FfiVecBytes32 = FfiVec; + +pub type FfiBlockBody = FfiVec; + +pub type FfiNonceList = FfiVec; + +pub type FfiInstructionDataList = FfiVec; + +pub type FfiSignaturePubKeyList = FfiVec; + +pub type FfiProof = FfiVecU8; + +pub type FfiProgramDeploymentMessage = FfiVecU8; + +pub type FfiEncryptedAccountDataList = FfiVec; + +pub type FfiNullifierCommitmentSetList = FfiVec; diff --git a/sequencer/core/src/indexer_client.rs b/indexer/ffi/src/client.rs similarity index 91% rename from sequencer/core/src/indexer_client.rs rename to indexer/ffi/src/client.rs index 960b77a4..f05b350e 100644 --- a/sequencer/core/src/indexer_client.rs +++ b/indexer/ffi/src/client.rs @@ -4,7 +4,6 @@ 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; } diff --git a/indexer_ffi/src/errors.rs b/indexer/ffi/src/errors.rs similarity index 94% rename from indexer_ffi/src/errors.rs rename to indexer/ffi/src/errors.rs index 46aa0f9f..4572474c 100644 --- a/indexer_ffi/src/errors.rs +++ b/indexer/ffi/src/errors.rs @@ -5,6 +5,7 @@ pub enum OperationStatus { Ok = 0x0, NullPointer = 0x1, InitializationError = 0x2, + ClientError = 0x3, } impl OperationStatus { diff --git a/indexer_ffi/src/indexer.rs b/indexer/ffi/src/indexer.rs similarity index 59% rename from indexer_ffi/src/indexer.rs rename to indexer/ffi/src/indexer.rs index c110b183..33800356 100644 --- a/indexer_ffi/src/indexer.rs +++ b/indexer/ffi/src/indexer.rs @@ -3,18 +3,26 @@ use std::{ffi::c_void, net::SocketAddr}; use indexer_service::IndexerHandle; use tokio::runtime::Runtime; +use crate::client::IndexerClient; + #[repr(C)] pub struct IndexerServiceFFI { indexer_handle: *mut c_void, runtime: *mut c_void, + indexer_client: *mut c_void, } impl IndexerServiceFFI { - pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self { + pub fn new( + indexer_handle: indexer_service::IndexerHandle, + runtime: Runtime, + indexer_client: IndexerClient, + ) -> Self { Self { // Box the complex types and convert to opaque pointers indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), runtime: Box::into_raw(Box::new(runtime)).cast::(), + indexer_client: Box::into_raw(Box::new(indexer_client)).cast::(), } } @@ -25,10 +33,11 @@ impl IndexerServiceFFI { /// The caller must ensure that: /// - `self` is a valid object(contains valid pointers in all fields) #[must_use] - pub unsafe fn into_parts(self) -> (Box, Box) { + pub unsafe fn into_parts(self) -> (Box, Box, Box) { let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::()) }; let runtime = unsafe { Box::from_raw(self.runtime.cast::()) }; - (indexer_handle, runtime) + let indexer_client = unsafe { Box::from_raw(self.indexer_client.cast::()) }; + (indexer_handle, runtime, indexer_client) } /// Helper to get indexer handle addr. @@ -49,7 +58,7 @@ impl IndexerServiceFFI { indexer_handle.addr() } - /// Helper to get indexer handle addr. + /// Helper to get indexer handle ref. /// /// # Safety /// @@ -64,6 +73,38 @@ impl IndexerServiceFFI { .expect("Indexer Handle must be non-null pointer") } } + + /// Helper to get indexer client ref. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn client(&self) -> &IndexerClient { + unsafe { + self.indexer_client + .cast::() + .as_ref() + .expect("Indexer Client must be non-null pointer") + } + } + + /// Helper to get indexer runtime ref. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn runtime(&self) -> &Runtime { + unsafe { + self.runtime + .cast::() + .as_ref() + .expect("Indexer Runtime must be non-null pointer") + } + } } // Implement Drop to prevent memory leaks @@ -72,6 +113,7 @@ impl Drop for IndexerServiceFFI { let Self { indexer_handle, runtime, + indexer_client, } = self; if indexer_handle.is_null() { @@ -80,7 +122,11 @@ impl Drop for IndexerServiceFFI { if runtime.is_null() { log::error!("Attempted to drop a null tokio runtime pointer. This is a bug"); } + if indexer_client.is_null() { + log::error!("Attempted to drop a null client pointer. This is a bug"); + } drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); drop(unsafe { Box::from_raw(runtime.cast::()) }); + drop(unsafe { Box::from_raw(indexer_client.cast::()) }); } } diff --git a/indexer_ffi/src/lib.rs b/indexer/ffi/src/lib.rs similarity index 93% rename from indexer_ffi/src/lib.rs rename to indexer/ffi/src/lib.rs index fe594ec0..5806a074 100644 --- a/indexer_ffi/src/lib.rs +++ b/indexer/ffi/src/lib.rs @@ -4,5 +4,6 @@ pub use errors::OperationStatus; pub use indexer::IndexerServiceFFI; pub mod api; +mod client; mod errors; mod indexer; 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/indexer/service/rpc/src/lib.rs b/indexer/service/rpc/src/lib.rs index 217c60d4..a6476ebd 100644 --- a/indexer/service/rpc/src/lib.rs +++ b/indexer/service/rpc/src/lib.rs @@ -41,6 +41,13 @@ pub trait Rpc { #[method(name = "getAccount")] async fn get_account(&self, account_id: AccountId) -> Result; + #[method(name = "getAccountAtBlock")] + async fn get_account_at_block( + &self, + account_id: AccountId, + block_id: BlockId, + ) -> Result; + #[method(name = "getTransaction")] async fn get_transaction( &self, diff --git a/indexer/service/src/mock_service.rs b/indexer/service/src/mock_service.rs index c4a099b8..a413973d 100644 --- a/indexer/service/src/mock_service.rs +++ b/indexer/service/src/mock_service.rs @@ -239,6 +239,22 @@ impl indexer_service_rpc::RpcServer for MockIndexerService { .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>)) } + async fn get_account_at_block( + &self, + account_id: AccountId, + _block_id: BlockId, + ) -> Result { + // Mock service does not track historical state; returns current state regardless of + // block_id. + self.state + .read() + .await + .accounts + .get(&account_id) + .cloned() + .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>)) + } + async fn get_transaction( &self, tx_hash: HashType, diff --git a/indexer/service/src/service.rs b/indexer/service/src/service.rs index e2f8a321..8d079265 100644 --- a/indexer/service/src/service.rs +++ b/indexer/service/src/service.rs @@ -83,6 +83,19 @@ impl indexer_service_rpc::RpcServer for IndexerService { .into()) } + async fn get_account_at_block( + &self, + account_id: AccountId, + block_id: BlockId, + ) -> Result { + Ok(self + .indexer + .store + .account_state_at_block(&account_id.into(), block_id) + .map_err(db_error)? + .into()) + } + async fn get_transaction( &self, tx_hash: HashType, diff --git a/indexer_ffi/indexer_ffi.h b/indexer_ffi/indexer_ffi.h deleted file mode 100644 index 7c7d9a4d..00000000 --- a/indexer_ffi/indexer_ffi.h +++ /dev/null @@ -1,76 +0,0 @@ -#include -#include -#include -#include - -typedef enum OperationStatus { - Ok = 0, - NullPointer = 1, - InitializationError = 2, -} OperationStatus; - -typedef struct IndexerServiceFFI { - void *indexer_handle; - void *runtime; -} IndexerServiceFFI; - -/** - * Simple wrapper around a pointer to a value or an error. - * - * Pointer is not guaranteed. You should check the error field before - * dereferencing the pointer. - */ -typedef struct PointerResult_IndexerServiceFFI__OperationStatus { - struct IndexerServiceFFI *value; - enum OperationStatus error; -} PointerResult_IndexerServiceFFI__OperationStatus; - -typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; - -/** - * Creates and starts an indexer based on the provided - * configuration file path. - * - * # Arguments - * - * - `config_path`: A pointer to a string representing the path to the configuration file. - * - `port`: Number representing a port, on which indexers RPC will start. - * - * # Returns - * - * An `InitializedIndexerServiceFFIResult` containing either a pointer to the - * initialized `IndexerServiceFFI` or an error code. - */ -InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); - -/** - * Stops and frees the resources associated with the given indexer service. - * - * # Arguments - * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. - * - * # Returns - * - * An `OperationStatus` indicating success or failure. - * - * # Safety - * - * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance - * - The `IndexerServiceFFI` instance was created by this library - * - The pointer will not be used after this function returns - */ -enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); - -/** - * # Safety - * It's up to the caller to pass a proper pointer, if somehow from c/c++ side - * this is called with a type which doesn't come from a returned `CString` it - * will cause a segfault. - */ -void free_cstring(char *block); - -bool is_ok(const enum OperationStatus *self); - -bool is_error(const enum OperationStatus *self); diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index 53f0ee98..5f1f1037 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -19,11 +19,13 @@ 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 +indexer_service_protocol.workspace = true url.workspace = true @@ -35,4 +37,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..5c381e0e 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}; @@ -59,12 +59,16 @@ impl InitialData { } let mut private_charlie_key_chain = KeyChain::new_os_random(); - let mut private_charlie_account_id = - AccountId::from((&private_charlie_key_chain.nullifier_public_key, 0)); + let mut private_charlie_account_id = AccountId::for_regular_private_account( + &private_charlie_key_chain.nullifier_public_key, + 0, + ); let mut private_david_key_chain = KeyChain::new_os_random(); - let mut private_david_account_id = - AccountId::from((&private_david_key_chain.nullifier_public_key, 0)); + let mut private_david_account_id = AccountId::for_regular_private_account( + &private_david_key_chain.nullifier_public_key, + 0, + ); // Ensure consistent ordering if private_charlie_account_id > private_david_account_id { @@ -164,35 +168,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 +194,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 +218,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 fcae2c71..7abd0897 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; @@ -26,6 +29,7 @@ pub mod test_context_ffi; pub const TIME_TO_WAIT_FOR_BLOCK_SECONDS: u64 = 12; pub const NSSA_PROGRAM_FOR_TEST_DATA_CHANGER: &str = "data_changer.bin"; pub const NSSA_PROGRAM_FOR_TEST_NOOP: &str = "noop.bin"; +pub const NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY: &str = "pda_fund_spend_proxy.bin"; const BEDROCK_SERVICE_WITH_OPEN_PORT: &str = "logos-blockchain-node-0"; const BEDROCK_SERVICE_PORT: u16 = 18080; @@ -77,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..d03a4e00 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")?; @@ -268,6 +266,11 @@ impl BlockingTestContextFFI { pub fn runtime_clone(&self) -> Arc { Arc::::clone(&self.runtime) } + + #[must_use] + pub const fn indexer_ffi(&self) -> *const IndexerServiceFFI { + &raw const (self.indexer_ffi) + } } impl Drop for BlockingTestContextFFI { diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index f08e3759..9fbc174b 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -627,14 +627,14 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { .await?; // Both accounts must be discovered with the correct balances. - let account_id_1 = AccountId::from((&npk, identifier_1)); + let account_id_1 = AccountId::for_regular_private_account(&npk, identifier_1); let acc_1 = ctx .wallet() .get_account_private(account_id_1) .context("account for identifier 1 not found after sync")?; assert_eq!(acc_1.balance, 100); - let account_id_2 = AccountId::from((&npk, identifier_2)); + let account_id_2 = AccountId::for_regular_private_account(&npk, identifier_2); let acc_2 = ctx .wallet() .get_account_private(account_id_2) diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs index 2730f9b5..ea0924d7 100644 --- a/integration_tests/tests/indexer_ffi.rs +++ b/integration_tests/tests/indexer_ffi.rs @@ -1,11 +1,18 @@ #![expect( clippy::shadow_unrelated, clippy::tests_outside_test_module, + clippy::undocumented_unsafe_blocks, reason = "We don't care about these in tests" )] use anyhow::{Context as _, Result}; -use indexer_service_rpc::RpcClient as _; +use indexer_ffi::{ + IndexerServiceFFI, OperationStatus, + api::{ + PointerResult, + types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock}, + }, +}; use integration_tests::{ TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id, test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state, @@ -17,6 +24,23 @@ use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcomma /// Maximum time to wait for the indexer to catch up to the sequencer. const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000; +unsafe extern "C" { + unsafe fn query_last_block( + indexer: *const IndexerServiceFFI, + ) -> PointerResult; + + unsafe fn query_block_vec( + indexer: *const IndexerServiceFFI, + before: FfiOption, + limit: u64, + ) -> PointerResult, OperationStatus>; + + unsafe fn query_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, + ) -> PointerResult; +} + #[test] fn indexer_test_run_ffi() -> Result<()> { let blocking_ctx = BlockingTestContextFFI::new()?; @@ -28,10 +52,19 @@ fn indexer_test_run_ffi() -> Result<()> { }); let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?; + let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + + assert!(last_block_indexer_ffi_res.error.is_ok()); + + let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; info!("Last block on ind now is {last_block_indexer}"); + info!("Last block on ind ffi now is {last_block_indexer_ffi}"); assert!(last_block_indexer > 1); + assert!(last_block_indexer_ffi > 1); + + assert_eq!(last_block_indexer, last_block_indexer_ffi); Ok(()) } @@ -40,7 +73,6 @@ fn indexer_test_run_ffi() -> Result<()> { fn indexer_ffi_block_batching() -> Result<()> { let blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime(); - let ctx = blocking_ctx.ctx(); // WAIT info!("Waiting for indexer to parse blocks"); @@ -48,31 +80,36 @@ fn indexer_ffi_block_batching() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let last_block_indexer = runtime_wrapped - .block_on(ctx.indexer_client().get_last_finalized_block_id()) - .unwrap(); + let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + + assert!(last_block_indexer_ffi_res.error.is_ok()); + + let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value }; info!("Last block on ind now is {last_block_indexer}"); assert!(last_block_indexer > 1); - // Getting wide batch to fit all blocks (from latest backwards) - let mut block_batch = runtime_wrapped - .block_on(ctx.indexer_client().get_blocks(None, 100)) - .unwrap(); + let before_ffi = FfiOption::::from_none(); + let limit = 100; - // Reverse to check chain consistency from oldest to newest - block_batch.reverse(); + let block_batch_ffi_res = + unsafe { query_block_vec(blocking_ctx.indexer_ffi(), before_ffi, limit) }; - // Checking chain consistency - let mut prev_block_hash = block_batch.first().unwrap().header.hash; + assert!(block_batch_ffi_res.error.is_ok()); - for block in &block_batch[1..] { - assert_eq!(block.header.prev_block_hash, prev_block_hash); + let block_batch = unsafe { &*block_batch_ffi_res.value }; + + let mut last_block_prev_hash = unsafe { block_batch.get(0) }.header.prev_block_hash.data; + + for i in 1..block_batch.len { + let block = unsafe { block_batch.get(i) }; + + assert_eq!(last_block_prev_hash, block.header.hash.data); info!("Block {} chain-consistent", block.header.block_id); - prev_block_hash = block.header.hash; + last_block_prev_hash = block.header.prev_block_hash.data; } Ok(()) @@ -82,6 +119,7 @@ fn indexer_ffi_block_batching() -> Result<()> { fn indexer_ffi_state_consistency() -> Result<()> { let mut blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime_clone(); + let indexer_ffi = blocking_ctx.indexer_ffi(); let ctx = blocking_ctx.ctx_mut(); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { @@ -179,14 +217,21 @@ fn indexer_ffi_state_consistency() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let acc1_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[0].into()), - )?; - let acc2_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[1].into()), - )?; + let acc1_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + + assert!(acc1_ind_state_ffi.error.is_ok()); + + let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; + let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); + + let acc2_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[1]).into()) }; + + assert!(acc2_ind_state_ffi.error.is_ok()); + + let acc2_ind_state_pre = unsafe { &*acc2_ind_state_ffi.value }; + let acc2_ind_state: indexer_service_protocol::Account = acc2_ind_state_pre.into(); info!("Checking correct state transition"); let acc1_seq_state = @@ -212,6 +257,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { fn indexer_ffi_state_consistency_with_labels() -> Result<()> { let mut blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime_clone(); + let indexer_ffi = blocking_ctx.indexer_ffi(); let ctx = blocking_ctx.ctx_mut(); // Assign labels to both accounts @@ -275,10 +321,14 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let acc1_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[0].into()), - )?; + let acc1_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + + assert!(acc1_ind_state_ffi.error.is_ok()); + + let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; + let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); + let acc1_seq_state = runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( ctx.sequencer_client(), diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs new file mode 100644 index 00000000..518239e7 --- /dev/null +++ b/integration_tests/tests/private_pda.rs @@ -0,0 +1,309 @@ +#![expect( + clippy::tests_outside_test_module, + reason = "We don't care about these in tests" +)] + +use std::{path::PathBuf, time::Duration}; + +use anyhow::{Context as _, Result}; +use integration_tests::{ + NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY, TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, + verify_commitment_is_in_state, +}; +use log::info; +use nssa::{ + AccountId, ProgramId, privacy_preserving_transaction::circuit::ProgramWithDependencies, + program::Program, +}; +use nssa_core::{NullifierPublicKey, encryption::ViewingPublicKey, program::PdaSeed}; +use tokio::test; +use wallet::{ + PrivacyPreservingAccount, WalletCore, + cli::{Command, account::AccountSubcommand}, +}; + +/// Funds a private PDA via the proxy program with a chained call to `auth_transfer`. +/// +/// A direct call to `auth_transfer` cannot establish the PDA-to-npk binding because it uses +/// `Claim::Authorized` rather than `Claim::Pda`. Routing through the proxy provides the binding +/// via `pda_seeds` in the chained call to `auth_transfer`. +#[expect( + clippy::too_many_arguments, + reason = "test helper — grouping args would obscure intent" +)] +async fn fund_private_pda( + wallet: &WalletCore, + sender: AccountId, + pda_account_id: AccountId, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: u128, + seed: PdaSeed, + amount: u128, + proxy_program: &ProgramWithDependencies, + auth_transfer_id: ProgramId, +) -> Result<()> { + wallet + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::Public(sender), + PrivacyPreservingAccount::PrivatePdaForeign { + account_id: pda_account_id, + npk, + vpk, + identifier, + }, + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, true)) + .context("failed to serialize pda_fund_spend_proxy fund instruction")?, + proxy_program, + ) + .await + .map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(()) +} + +/// Spends from an owned private PDA to a fresh private-foreign recipient. +/// +/// Alice must own the PDA in the wallet (i.e. it must have been synced after a receive). +#[expect( + clippy::too_many_arguments, + reason = "test helper — grouping args would obscure intent" +)] +async fn spend_private_pda( + wallet: &WalletCore, + pda_account_id: AccountId, + recipient_npk: NullifierPublicKey, + recipient_vpk: ViewingPublicKey, + seed: PdaSeed, + amount: u128, + spend_program: &ProgramWithDependencies, + auth_transfer_id: nssa::ProgramId, +) -> Result<()> { + wallet + .send_privacy_preserving_tx( + vec![ + PrivacyPreservingAccount::PrivatePdaOwned(pda_account_id), + PrivacyPreservingAccount::PrivateForeign { + npk: recipient_npk, + vpk: recipient_vpk, + identifier: 0, + }, + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, false)) + .context("failed to serialize pda_fund_spend_proxy instruction")?, + spend_program, + ) + .await + .map_err(|e| anyhow::anyhow!("{e}"))?; + Ok(()) +} + +/// Two private transfers go to distinct members of the same PDA family (same seed and npk, +/// but identifier=0 and identifier=1). Alice then spends from both PDAs. +/// +/// This exercises the full identifier-diversified private PDA lifecycle: +/// receive(id=0), receive(id=1) → sync → spend(id=0), spend(id=1) → sync → assert. +#[test] +async fn private_pda_family_members_receive_and_spend() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // ── Build alice's key chain ────────────────────────────────────────────────────────────────── + let alice_chain_index = ctx.wallet_mut().create_private_accounts_key(None); + let (alice_npk, alice_vpk) = { + let node = ctx + .wallet() + .storage() + .user_data + .private_key_tree + .key_map + .get(&alice_chain_index) + .context("key node was just inserted")?; + let kc = &node.value.0; + (kc.nullifier_public_key, kc.viewing_public_key.clone()) + }; + + let proxy = { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../artifacts/test_program_methods") + .join(NSSA_PROGRAM_FOR_TEST_PDA_FUND_SPEND_PROXY); + Program::new(std::fs::read(&path).with_context(|| format!("reading {path:?}"))?) + .context("invalid pda_fund_spend_proxy binary")? + }; + let auth_transfer = Program::authenticated_transfer_program(); + let proxy_id = proxy.id(); + let auth_transfer_id = auth_transfer.id(); + let seed = PdaSeed::new([42; 32]); + let amount: u128 = 100; + + let spend_program = + ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); + + let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); + let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); + + // Use two different public senders to avoid nonce conflicts between the back-to-back txs. + let senders = ctx.existing_public_accounts(); + let sender_0 = senders[0]; + let sender_1 = senders[1]; + + // ── Receive ────────────────────────────────────────────────────────────────────────────────── + + info!("Sending to alice_pda_0 (identifier=0)"); + fund_private_pda( + ctx.wallet(), + sender_0, + alice_pda_0_id, + alice_npk, + alice_vpk.clone(), + 0, + seed, + amount, + &spend_program, + auth_transfer_id, + ) + .await?; + + info!("Sending to alice_pda_1 (identifier=1)"); + fund_private_pda( + ctx.wallet(), + sender_1, + alice_pda_1_id, + alice_npk, + alice_vpk.clone(), + 1, + seed, + amount, + &spend_program, + auth_transfer_id, + ) + .await?; + + info!("Waiting for block"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Sync so alice's wallet discovers and stores both PDAs. + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::SyncPrivate {}), + ) + .await?; + + // Both PDAs must be discoverable and have the correct balance. + let pda_0_account = ctx + .wallet() + .get_account_private(alice_pda_0_id) + .context("alice_pda_0 not found after sync")?; + assert_eq!(pda_0_account.balance, amount); + + let pda_1_account = ctx + .wallet() + .get_account_private(alice_pda_1_id) + .context("alice_pda_1 not found after sync")?; + assert_eq!(pda_1_account.balance, amount); + + // Commitments for both PDAs must be in the sequencer's state. + let commitment_0 = ctx + .wallet() + .get_private_account_commitment(alice_pda_0_id) + .context("commitment for alice_pda_0 missing")?; + assert!( + verify_commitment_is_in_state(commitment_0.clone(), ctx.sequencer_client()).await, + "alice_pda_0 commitment not in state after receive" + ); + + let commitment_1 = ctx + .wallet() + .get_private_account_commitment(alice_pda_1_id) + .context("commitment for alice_pda_1 missing")?; + assert!( + verify_commitment_is_in_state(commitment_1.clone(), ctx.sequencer_client()).await, + "alice_pda_1 commitment not in state after receive" + ); + assert_ne!( + commitment_0, commitment_1, + "distinct identifiers must yield distinct commitments" + ); + + // ── Spend ───────────────────────────────────────────────────────────────────────────────────── + + // Fresh recipients — hardcoded npks not in any wallet. + let recipient_npk_0 = NullifierPublicKey([0xAA; 32]); + let recipient_vpk_0 = ViewingPublicKey::from_scalar(recipient_npk_0.0); + + let recipient_npk_1 = NullifierPublicKey([0xBB; 32]); + let recipient_vpk_1 = ViewingPublicKey::from_scalar(recipient_npk_1.0); + + let amount_spend_0: u128 = 13; + let amount_spend_1: u128 = 37; + + info!("Alice spending from alice_pda_0"); + spend_private_pda( + ctx.wallet(), + alice_pda_0_id, + recipient_npk_0, + recipient_vpk_0, + seed, + amount_spend_0, + &spend_program, + auth_transfer_id, + ) + .await?; + + info!("Alice spending from alice_pda_1"); + spend_private_pda( + ctx.wallet(), + alice_pda_1_id, + recipient_npk_1, + recipient_vpk_1, + seed, + amount_spend_1, + &spend_program, + auth_transfer_id, + ) + .await?; + + info!("Waiting for block"); + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Account(AccountSubcommand::SyncPrivate {}), + ) + .await?; + + // After spending, PDAs should have the remaining balance. + let pda_0_spent = ctx + .wallet() + .get_account_private(alice_pda_0_id) + .context("alice_pda_0 not found after spend sync")?; + assert_eq!(pda_0_spent.balance, amount - amount_spend_0); + + let pda_1_spent = ctx + .wallet() + .get_account_private(alice_pda_1_id) + .context("alice_pda_1 not found after spend sync")?; + assert_eq!(pda_1_spent.balance, amount - amount_spend_1); + + // Post-spend commitments must be in state. + let post_spend_commitment_0 = ctx + .wallet() + .get_private_account_commitment(alice_pda_0_id) + .context("post-spend commitment for alice_pda_0 missing")?; + assert!( + verify_commitment_is_in_state(post_spend_commitment_0, ctx.sequencer_client()).await, + "alice_pda_0 post-spend commitment not in state" + ); + + let post_spend_commitment_1 = ctx + .wallet() + .get_private_account_commitment(alice_pda_1_id) + .context("post-spend commitment for alice_pda_1 missing")?; + assert!( + verify_commitment_is_in_state(post_spend_commitment_1, ctx.sequencer_client()).await, + "alice_pda_1 post-spend commitment not in state" + ); + + info!("Private PDA family member receive-and-spend test passed"); + Ok(()) +} diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs new file mode 100644 index 00000000..80ea1bd4 --- /dev/null +++ b/integration_tests/tests/shared_accounts.rs @@ -0,0 +1,234 @@ +#![expect( + clippy::tests_outside_test_module, + reason = "Integration test file, not inside a #[cfg(test)] module" +)] +#![expect( + clippy::shadow_unrelated, + reason = "Sequential wallet commands naturally reuse the `command` binding" +)] + +//! Shared account integration tests. +//! +//! Demonstrates: +//! 1. Group creation and GMS distribution via seal/unseal. +//! 2. Shared regular private account creation via `--for-gms`. +//! 3. Funding a shared account from a public account. +//! 4. Syncing discovers the funded shared account state. + +use std::time::Duration; + +use anyhow::{Context as _, Result}; +use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, format_public_account_id}; +use log::info; +use tokio::test; +use wallet::cli::{ + Command, SubcommandReturnValue, + account::{AccountSubcommand, NewSubcommand}, + group::GroupSubcommand, + programs::native_token_transfer::AuthTransferSubcommand, +}; + +/// Create a group, create a shared account from it, and verify registration. +#[test] +async fn group_create_and_shared_account_registration() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Create a group + let command = Command::Group(GroupSubcommand::New { + name: "test-group".into(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Verify group exists + assert!( + ctx.wallet() + .storage() + .user_data + .group_key_holder("test-group") + .is_some() + ); + + // Create a shared regular private account from the group + let command = Command::Account(AccountSubcommand::New(NewSubcommand::PrivateGms { + group: "test-group".into(), + label: Some("shared-acc".into()), + pda: false, + seed: None, + program_id: None, + identifier: None, + })); + + let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::RegisterAccount { + account_id: shared_account_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Verify shared account is registered in storage + let entry = ctx + .wallet() + .storage() + .user_data + .shared_private_account(&shared_account_id) + .context("Shared account not found in storage")?; + assert_eq!(entry.group_label, "test-group"); + assert!(entry.pda_seed.is_none()); + + info!("Shared account registered: {shared_account_id}"); + Ok(()) +} + +/// GMS seal/unseal round-trip via invite/join, verify key agreement. +#[test] +async fn group_invite_join_key_agreement() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Generate a sealing key + let command = Command::Group(GroupSubcommand::NewSealingKey); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Create a group + let command = Command::Group(GroupSubcommand::New { + name: "alice-group".into(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Seal GMS for ourselves (simulating invite to another wallet) + let sealing_sk = ctx + .wallet() + .storage() + .user_data + .sealing_secret_key + .context("Sealing key not found")?; + let sealing_pk = + key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); + + let holder = ctx + .wallet() + .storage() + .user_data + .group_key_holder("alice-group") + .context("Group not found")?; + let sealed = holder.seal_for(&sealing_pk); + let sealed_hex = hex::encode(&sealed); + + // Join under a different name (simulating Bob receiving the sealed GMS) + let command = Command::Group(GroupSubcommand::Join { + name: "bob-copy".into(), + sealed: sealed_hex, + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Both derive the same keys for the same derivation seed + let alice_holder = ctx + .wallet() + .storage() + .user_data + .group_key_holder("alice-group") + .unwrap(); + let bob_holder = ctx + .wallet() + .storage() + .user_data + .group_key_holder("bob-copy") + .unwrap(); + + let seed = [42_u8; 32]; + let alice_npk = alice_holder + .derive_keys_for_shared_account(&seed) + .generate_nullifier_public_key(); + let bob_npk = bob_holder + .derive_keys_for_shared_account(&seed) + .generate_nullifier_public_key(); + + assert_eq!( + alice_npk, bob_npk, + "Key agreement: same GMS produces same keys" + ); + + info!("Key agreement verified via invite/join"); + Ok(()) +} + +/// Fund a shared account from a public account via auth-transfer, then sync. +/// TODO: Requires auth-transfer init to work with shared accounts (authorization flow). +#[test] +async fn fund_shared_account_from_public() -> Result<()> { + let mut ctx = TestContext::new().await?; + + // Create group and shared account + let command = Command::Group(GroupSubcommand::New { + name: "fund-group".into(), + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + let command = Command::Account(AccountSubcommand::New(NewSubcommand::PrivateGms { + group: "fund-group".into(), + label: None, + pda: false, + seed: None, + program_id: None, + identifier: None, + })); + let result = wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + let SubcommandReturnValue::RegisterAccount { + account_id: shared_id, + } = result + else { + anyhow::bail!("Expected RegisterAccount return value"); + }; + + // Initialize the shared account under auth-transfer + let command = Command::AuthTransfer(AuthTransferSubcommand::Init { + account_id: Some(format!("Private/{shared_id}")), + account_label: None, + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Sync private accounts + let command = Command::Account(AccountSubcommand::SyncPrivate); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Fund from a public account + let from_public = ctx.existing_public_accounts()[0]; + let command = Command::AuthTransfer(AuthTransferSubcommand::Send { + from: Some(format_public_account_id(from_public)), + from_label: None, + to: Some(format!("Private/{shared_id}")), + to_label: None, + to_npk: None, + to_vpk: None, + to_identifier: None, + amount: 100, + }); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; + + // Sync private accounts + let command = Command::Account(AccountSubcommand::SyncPrivate); + wallet::cli::execute_subcommand(ctx.wallet_mut(), command).await?; + + // Verify the shared account was updated + let entry = ctx + .wallet() + .storage() + .user_data + .shared_private_account(&shared_id) + .context("Shared account not found after sync")?; + + info!( + "Shared account balance after funding: {}", + entry.account.balance + ); + assert_eq!( + entry.account.balance, 100, + "Shared account should have received 100" + ); + + Ok(()) +} diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index 41de30ed..1f132932 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, }; @@ -220,7 +220,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { data: Data::default(), }, true, - AccountId::from((&sender_npk, 0)), + AccountId::for_regular_private_account(&sender_npk, 0), ); let recipient_nsk = [2; 32]; let recipient_vsk = [99; 32]; @@ -229,7 +229,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { let recipient_pre = AccountWithMetadata::new( Account::default(), false, - AccountId::from((&recipient_npk, 0)), + AccountId::for_regular_private_account(&recipient_npk, 0), ); let eph_holder_from = EphemeralKeyHolder::new(&sender_npk); @@ -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/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index db84b066..a0904a9a 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -801,7 +801,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> { let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys); - let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128)); + let account_id = nssa::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); let to: FfiBytes32 = (&account_id).into(); (to, out_keys) }; @@ -935,7 +935,7 @@ fn test_wallet_ffi_transfer_private() -> Result<()> { let (to, to_keys) = unsafe { let mut out_keys = FfiPrivateAccountKeys::default(); wallet_ffi_create_private_accounts_key(wallet_ffi_handle, &raw mut out_keys); - let account_id = nssa::AccountId::from((&out_keys.npk(), 0_u128)); + let account_id = nssa::AccountId::for_regular_private_account(&out_keys.npk(), 0_u128); let to: FfiBytes32 = (&account_id).into(); (to, out_keys) }; 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..8bc9ed13 --- /dev/null +++ b/key_protocol/src/key_management/group_key_holder.rs @@ -0,0 +1,601 @@ +use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; +use nssa_core::{ + SharedSecretKey, + encryption::{Scalar, shared_key_derivation::Secp256k1Point}, + program::{PdaSeed, ProgramId}, +}; +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. +/// +/// Wraps a secp256k1 point but is a distinct type from `ViewingPublicKey` to enforce +/// key separation: viewing keys encrypt account state, sealing keys encrypt the GMS +/// for off-chain distribution. +pub struct SealingPublicKey(Secp256k1Point); + +impl SealingPublicKey { + /// Derive the sealing public key from a secret scalar. + #[must_use] + pub fn from_scalar(scalar: Scalar) -> Self { + Self(Secp256k1Point::from_scalar(scalar)) + } + + /// Construct from raw serialized bytes (e.g. received from another wallet). + #[must_use] + pub const fn from_bytes(bytes: Vec) -> Self { + Self(Secp256k1Point(bytes)) + } + + /// Returns the raw bytes for display or transmission. + #[must_use] + pub fn to_bytes(&self) -> &[u8] { + &self.0.0 + } +} + +/// 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 `(program_id, pda_seed)` pair 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, + program_id: &ProgramId, + 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); + for word in program_id { + hasher.update(word.to_le_bytes()); + } + hasher.update(pda_seed.as_ref()); + SecretSpendingKey(hasher.finalize_fixed().into()) + } + + /// Derive keys for a specific PDA under a given program. + /// + /// All controllers holding the same GMS independently derive the same keys for the + /// same `(program_id, seed)` because the derivation is deterministic. + #[must_use] + pub fn derive_keys_for_pda( + &self, + program_id: &ProgramId, + pda_seed: &PdaSeed, + ) -> PrivateKeyHolder { + self.secret_spending_key_for_pda(program_id, pda_seed) + .produce_private_key_holder(None) + } + + /// Derive keys for a shared regular (non-PDA) private account. + /// + /// Uses a distinct domain separator from `derive_keys_for_pda` to prevent cross-domain + /// key collisions. The `derivation_seed` should be a stable, unique 32-byte value + /// (e.g. derived deterministically from the account's identifier). + #[must_use] + pub fn derive_keys_for_shared_account(&self, derivation_seed: &[u8; 32]) -> PrivateKeyHolder { + const PREFIX: &[u8; 32] = b"/LEE/v0.3/GroupKeyDerivation/SHA"; + let mut hasher = sha2::Sha256::new(); + hasher.update(PREFIX); + hasher.update(self.gms); + hasher.update(derivation_seed); + SecretSpendingKey(hasher.finalize_fixed().into()).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.0); + 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::*; + + const TEST_PROGRAM_ID: ProgramId = [9; 8]; + + /// 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(&TEST_PROGRAM_ID, &seed); + let keys_b = holder_b.derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &seed_a) + .generate_nullifier_public_key(); + let npk_b = holder + .derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &seed) + .generate_nullifier_public_key(); + let npk_b = holder_b + .derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &seed) + .generate_nullifier_public_key(); + let npk_restored = restored + .derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &seed) + .generate_nullifier_public_key(); + let account_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX); + + let expected_npk = NullifierPublicKey([ + 136, 176, 234, 71, 208, 8, 143, 142, 126, 155, 132, 18, 71, 27, 88, 56, 100, 90, 79, + 215, 76, 92, 60, 166, 104, 35, 51, 91, 16, 114, 188, 112, + ]); + // AccountId is derived from (program_id, seed, npk), so it changes when npk changes. + // We verify npk is pinned, and AccountId is deterministically derived from it. + let expected_account_id = + AccountId::for_private_pda(&program_id, &seed, &expected_npk, u128::MAX); + + 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(&TEST_PROGRAM_ID, &seed) + .generate_nullifier_public_key(); + let npk_restored = restored + .derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &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(&SealingPublicKey::from_bytes(recipient_vpk.0)); + 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(&TEST_PROGRAM_ID, &seed) + .generate_nullifier_public_key(), + restored + .derive_keys_for_pda(&TEST_PROGRAM_ID, &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(&SealingPublicKey::from_bytes(recipient_vpk.0)); + 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(&SealingPublicKey::from_bytes(recipient_vpk.0)); + // 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 sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0); + let sealed_a = holder.seal_for(&sealing_key); + let sealed_b = holder.seal_for(&sealing_key); + 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(&TEST_PROGRAM_ID, &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(&TEST_PROGRAM_ID, &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(&SealingPublicKey::from_bytes(bob_vpk.0)); + 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(&TEST_PROGRAM_ID, &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); + } + + /// Same GMS + same derivation seed produces same keys for shared accounts. + #[test] + fn shared_account_same_gms_same_seed_produces_same_keys() { + let gms = [42_u8; 32]; + let derivation_seed = [1_u8; 32]; + let holder_a = GroupKeyHolder::from_gms(gms); + let holder_b = GroupKeyHolder::from_gms(gms); + + let npk_a = holder_a + .derive_keys_for_shared_account(&derivation_seed) + .generate_nullifier_public_key(); + let npk_b = holder_b + .derive_keys_for_shared_account(&derivation_seed) + .generate_nullifier_public_key(); + + assert_eq!(npk_a, npk_b); + } + + /// Different derivation seeds produce different keys for shared accounts. + #[test] + fn shared_account_different_seeds_produce_different_keys() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + let npk_a = holder + .derive_keys_for_shared_account(&[1_u8; 32]) + .generate_nullifier_public_key(); + let npk_b = holder + .derive_keys_for_shared_account(&[2_u8; 32]) + .generate_nullifier_public_key(); + + assert_ne!(npk_a, npk_b); + } + + /// PDA and shared account derivations from the same GMS + same bytes never collide. + #[test] + fn pda_and_shared_derivations_do_not_collide() { + let holder = GroupKeyHolder::from_gms([42_u8; 32]); + let bytes = [1_u8; 32]; + + let pda_npk = holder + .derive_keys_for_pda(&TEST_PROGRAM_ID, &PdaSeed::new(bytes)) + .generate_nullifier_public_key(); + let shared_npk = holder + .derive_keys_for_shared_account(&bytes) + .generate_nullifier_public_key(); + + assert_ne!(pda_npk, shared_npk); + } +} diff --git a/key_protocol/src/key_management/key_tree/keys_private.rs b/key_protocol/src/key_management/key_tree/keys_private.rs index 6ffc8119..05a7c996 100644 --- a/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,5 +1,5 @@ use k256::{Scalar, elliptic_curve::PrimeField as _}; -use nssa_core::{Identifier, NullifierPublicKey, encryption::ViewingPublicKey}; +use nssa_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; use crate::key_management::{ @@ -10,7 +10,7 @@ use crate::key_management::{ #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ChildKeysPrivate { - pub value: (KeyChain, Vec<(Identifier, nssa::Account)>), + pub value: (KeyChain, Vec<(PrivateAccountKind, nssa::Account)>), pub ccc: [u8; 32], /// Can be [`None`] if root. pub cci: Option, @@ -115,9 +115,11 @@ impl KeyTreeNode for ChildKeysPrivate { } fn account_ids(&self) -> impl Iterator { - self.value.1.iter().map(|(identifier, _)| { - nssa::AccountId::from((&self.value.0.nullifier_public_key, *identifier)) - }) + let npk = self.value.0.nullifier_public_key; + self.value + .1 + .iter() + .map(move |(kind, _)| nssa::AccountId::for_private_account(&npk, kind)) } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 0ae0a52f..edf9dadd 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -274,7 +274,10 @@ impl KeyTree { identifier: Identifier, ) -> Option { let node = self.key_map.get(cci)?; - let account_id = nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier)); + let account_id = nssa::AccountId::for_regular_private_account( + &node.value.0.nullifier_public_key, + identifier, + ); if self.account_id_map.contains_key(&account_id) { return None; } @@ -319,6 +322,7 @@ mod tests { use std::{collections::HashSet, str::FromStr as _}; use nssa::AccountId; + use nssa_core::PrivateAccountKind; use super::*; @@ -532,7 +536,7 @@ mod tests { .get_mut(&ChainIndex::from_str("/1").unwrap()) .unwrap(); acc.value.1.push(( - 0, + PrivateAccountKind::Regular(0), nssa::Account { balance: 2, ..nssa::Account::default() @@ -544,7 +548,7 @@ mod tests { .get_mut(&ChainIndex::from_str("/2").unwrap()) .unwrap(); acc.value.1.push(( - 0, + PrivateAccountKind::Regular(0), nssa::Account { balance: 3, ..nssa::Account::default() @@ -556,7 +560,7 @@ mod tests { .get_mut(&ChainIndex::from_str("/0/1").unwrap()) .unwrap(); acc.value.1.push(( - 0, + PrivateAccountKind::Regular(0), nssa::Account { balance: 5, ..nssa::Account::default() @@ -568,7 +572,7 @@ mod tests { .get_mut(&ChainIndex::from_str("/1/0").unwrap()) .unwrap(); acc.value.1.push(( - 0, + PrivateAccountKind::Regular(0), nssa::Account { balance: 6, ..nssa::Account::default() 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 4df6df82..b228e3ef 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -3,11 +3,12 @@ use std::collections::BTreeMap; use anyhow::Result; use k256::AffinePoint; use nssa::{Account, AccountId}; -use nssa_core::Identifier; +use nssa_core::{Identifier, PrivateAccountKind}; use serde::{Deserialize, Serialize}; use crate::key_management::{ KeyChain, + group_key_holder::GroupKeyHolder, key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex}, secret_holders::SeedHolder, }; @@ -17,10 +18,25 @@ pub type PublicKey = AffinePoint; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserPrivateAccountData { pub key_chain: KeyChain, - pub accounts: Vec<(Identifier, Account)>, + pub accounts: Vec<(PrivateAccountKind, Account)>, } +/// Metadata for a shared account (GMS-derived), stored alongside the cached plaintext state. +/// The group label and identifier (or PDA seed) are needed to re-derive keys during sync. #[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SharedAccountEntry { + pub group_label: String, + pub identifier: Identifier, + /// For PDA accounts, the seed and program ID used to derive keys via `derive_keys_for_pda`. + /// `None` for regular shared accounts (keys derived from identifier via derivation seed). + #[serde(default)] + pub pda_seed: Option, + #[serde(default)] + pub pda_program_id: Option, + pub account: Account, +} + +#[derive(Clone, Debug)] pub struct NSSAUserData { /// Default public accounts. pub default_pub_account_signing_keys: BTreeMap, @@ -30,6 +46,16 @@ pub struct NSSAUserData { pub public_key_tree: KeyTreePublic, /// Tree of private keys. pub private_key_tree: KeyTreePrivate, + /// Group key holders for shared account management, keyed by a human-readable label. + pub group_key_holders: BTreeMap, + /// Cached plaintext state of shared private accounts (PDAs and regular shared accounts), + /// keyed by `AccountId`. Each entry stores the group label and identifier needed + /// to re-derive keys during sync. + pub shared_private_accounts: BTreeMap, + /// Dedicated sealing secret key for GMS distribution. Generated once via + /// `wallet group new-sealing-key`. The corresponding public key is shared with + /// group members so they can seal GMS for this wallet. + pub sealing_secret_key: Option, } impl NSSAUserData { @@ -53,10 +79,11 @@ impl NSSAUserData { ) -> bool { let mut check_res = true; for (account_id, entry) in accounts_keys_map { - let any_match = entry.accounts.iter().any(|(identifier, _)| { - nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier)) - == *account_id - }); + let npk = &entry.key_chain.nullifier_public_key; + let any_match = entry + .accounts + .iter() + .any(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == *account_id); if !any_match { println!("No matching entry found for account_id {account_id}"); check_res = false; @@ -88,6 +115,9 @@ impl NSSAUserData { default_user_private_accounts: default_accounts_key_chains, public_key_tree, private_key_tree, + group_key_holders: BTreeMap::new(), + shared_private_accounts: BTreeMap::new(), + sealing_secret_key: None, }) } @@ -148,6 +178,7 @@ impl NSSAUserData { } /// Returns the key chain and account data for the given private account ID. + /// Does not cover shared private accounts — use `shared_private_account` for those. #[must_use] pub fn get_private_account( &self, @@ -155,24 +186,27 @@ impl NSSAUserData { ) -> Option<(KeyChain, nssa_core::account::Account, Identifier)> { // Check default accounts if let Some(entry) = self.default_user_private_accounts.get(&account_id) { - for (identifier, account) in &entry.accounts { - let expected_id = - nssa::AccountId::from((&entry.key_chain.nullifier_public_key, *identifier)); - if expected_id == account_id { - return Some((entry.key_chain.clone(), account.clone(), *identifier)); - } + let npk = &entry.key_chain.nullifier_public_key; + if let Some((kind, account)) = entry + .accounts + .iter() + .find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id) + { + return Some((entry.key_chain.clone(), account.clone(), kind.identifier())); } return None; } // Check tree if let Some(node) = self.private_key_tree.get_node(account_id) { let key_chain = &node.value.0; - for (identifier, account) in &node.value.1 { - let expected_id = - nssa::AccountId::from((&key_chain.nullifier_public_key, *identifier)); - if expected_id == account_id { - return Some((key_chain.clone(), account.clone(), *identifier)); - } + let npk = &key_chain.nullifier_public_key; + if let Some((kind, account)) = node + .value + .1 + .iter() + .find(|(kind, _)| nssa::AccountId::for_private_account(npk, kind) == account_id) + { + return Some((key_chain.clone(), account.clone(), kind.identifier())); } } None @@ -195,6 +229,56 @@ 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); + } + + /// Returns the cached account for a shared private account, if it exists. + #[must_use] + pub fn shared_private_account( + &self, + account_id: &nssa::AccountId, + ) -> Option<&SharedAccountEntry> { + self.shared_private_accounts.get(account_id) + } + + /// Inserts or replaces a shared private account entry. + pub fn insert_shared_private_account( + &mut self, + account_id: nssa::AccountId, + entry: SharedAccountEntry, + ) { + self.shared_private_accounts.insert(account_id, entry); + } + + /// Updates the cached account state for a shared private account. + pub fn update_shared_private_account_state( + &mut self, + account_id: &nssa::AccountId, + account: nssa_core::account::Account, + ) { + if let Some(entry) = self.shared_private_accounts.get_mut(account_id) { + entry.account = account; + } + } + + /// Iterates over all shared private accounts. + pub fn shared_private_accounts_iter( + &self, + ) -> impl Iterator { + self.shared_private_accounts.iter() + } } impl Default for NSSAUserData { @@ -214,6 +298,112 @@ 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()); + assert!(user_data.shared_private_accounts.is_empty()); + } + + #[test] + fn shared_account_entry_serde_round_trip() { + use nssa_core::program::PdaSeed; + + let entry = SharedAccountEntry { + group_label: String::from("test-group"), + identifier: 42, + pda_seed: None, + pda_program_id: None, + account: nssa_core::account::Account::default(), + }; + let encoded = bincode::serialize(&entry).expect("serialize"); + let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize"); + assert_eq!(decoded.group_label, "test-group"); + assert_eq!(decoded.identifier, 42); + assert!(decoded.pda_seed.is_none()); + + let pda_entry = SharedAccountEntry { + group_label: String::from("pda-group"), + identifier: u128::MAX, + pda_seed: Some(PdaSeed::new([7_u8; 32])), + pda_program_id: Some([9; 8]), + account: nssa_core::account::Account::default(), + }; + let pda_encoded = bincode::serialize(&pda_entry).expect("serialize pda"); + let pda_decoded: SharedAccountEntry = + bincode::deserialize(&pda_encoded).expect("deserialize pda"); + assert_eq!(pda_decoded.group_label, "pda-group"); + assert_eq!(pda_decoded.identifier, u128::MAX); + assert_eq!(pda_decoded.pda_seed.unwrap(), PdaSeed::new([7_u8; 32])); + } + + #[test] + fn shared_account_entry_none_pda_seed_round_trips() { + // Verify that an entry with pda_seed=None serializes and deserializes correctly, + // confirming the #[serde(default)] attribute works for backward compatibility. + let entry = SharedAccountEntry { + group_label: String::from("old"), + identifier: 1, + pda_seed: None, + pda_program_id: None, + account: nssa_core::account::Account::default(), + }; + let encoded = bincode::serialize(&entry).expect("serialize"); + let decoded: SharedAccountEntry = bincode::deserialize(&encoded).expect("deserialize"); + assert_eq!(decoded.group_label, "old"); + assert_eq!(decoded.identifier, 1); + assert!(decoded.pda_seed.is_none()); + } + + #[test] + fn shared_account_derives_consistent_keys_from_group() { + use nssa_core::program::PdaSeed; + + let mut user_data = NSSAUserData::default(); + let gms_holder = GroupKeyHolder::from_gms([42_u8; 32]); + user_data.insert_group_key_holder(String::from("my-group"), gms_holder); + + let holder = user_data.group_key_holder("my-group").unwrap(); + + // Regular shared account: derive via tag + let tag = [1_u8; 32]; + let keys_a = holder.derive_keys_for_shared_account(&tag); + let keys_b = holder.derive_keys_for_shared_account(&tag); + assert_eq!( + keys_a.generate_nullifier_public_key(), + keys_b.generate_nullifier_public_key(), + ); + + // PDA shared account: derive via seed + let seed = PdaSeed::new([2_u8; 32]); + let pda_keys_a = holder.derive_keys_for_pda(&[9; 8], &seed); + let pda_keys_b = holder.derive_keys_for_pda(&[9; 8], &seed); + assert_eq!( + pda_keys_a.generate_nullifier_public_key(), + pda_keys_b.generate_nullifier_public_key(), + ); + + // PDA and shared derivations don't collide + assert_ne!( + keys_a.generate_nullifier_public_key(), + pda_keys_a.generate_nullifier_public_key(), + ); + } + #[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..63c188ef 100644 --- a/nssa/core/src/circuit_io.rs +++ b/nssa/core/src/circuit_io.rs @@ -12,23 +12,99 @@ 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::for_regular_private_account(&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/encryption/mod.rs b/nssa/core/src/encryption/mod.rs index 80d62f30..4b675d0e 100644 --- a/nssa/core/src/encryption/mod.rs +++ b/nssa/core/src/encryption/mod.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}; -use crate::{Commitment, Identifier, account::Account}; +use crate::{Commitment, account::Account, program::PrivateAccountKind}; #[cfg(feature = "host")] pub mod shared_key_derivation; @@ -40,13 +40,14 @@ impl EncryptionScheme { #[must_use] pub fn encrypt( account: &Account, - identifier: Identifier, + kind: &PrivateAccountKind, shared_secret: &SharedSecretKey, commitment: &Commitment, output_index: u32, ) -> Ciphertext { - // Plaintext: identifier (16 bytes, little-endian) || account bytes - let mut buffer = identifier.to_le_bytes().to_vec(); + // Plaintext: PrivateAccountKind::HEADER_LEN bytes header || account bytes. + // Both variants produce the same header length — see PrivateAccountKind::to_header_bytes. + let mut buffer = kind.to_header_bytes().to_vec(); buffer.extend_from_slice(&account.to_bytes()); Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index); Ciphertext(buffer) @@ -89,17 +90,19 @@ impl EncryptionScheme { shared_secret: &SharedSecretKey, commitment: &Commitment, output_index: u32, - ) -> Option<(Identifier, Account)> { + ) -> Option<(PrivateAccountKind, Account)> { use std::io::Cursor; let mut buffer = ciphertext.0.clone(); Self::symmetric_transform(&mut buffer, shared_secret, commitment, output_index); - if buffer.len() < 16 { + if buffer.len() < PrivateAccountKind::HEADER_LEN { return None; } - let identifier = Identifier::from_le_bytes(buffer[..16].try_into().unwrap()); + let header: &[u8; PrivateAccountKind::HEADER_LEN] = + buffer[..PrivateAccountKind::HEADER_LEN].try_into().unwrap(); + let kind = PrivateAccountKind::from_header_bytes(header)?; - let mut cursor = Cursor::new(&buffer[16..]); + let mut cursor = Cursor::new(&buffer[PrivateAccountKind::HEADER_LEN..]); Account::from_cursor(&mut cursor) .inspect_err(|err| { println!( @@ -112,6 +115,43 @@ impl EncryptionScheme { ); }) .ok() - .map(|account| (identifier, account)) + .map(|account| (kind, account)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + account::{Account, AccountId}, + program::PdaSeed, + }; + + #[test] + fn encrypt_same_length_for_account_and_pda() { + let account = Account::default(); + let secret = SharedSecretKey([0_u8; 32]); + let commitment = crate::Commitment::new(&AccountId::new([0_u8; 32]), &Account::default()); + + let account_ct = EncryptionScheme::encrypt( + &account, + &PrivateAccountKind::Regular(42), + &secret, + &commitment, + 0, + ); + let pda_ct = EncryptionScheme::encrypt( + &account, + &PrivateAccountKind::Pda { + program_id: [1_u32; 8], + seed: PdaSeed::new([2_u8; 32]), + identifier: 42, + }, + &secret, + &commitment, + 0, + ); + + assert_eq!(account_ct.0.len(), pda_ct.0.len()); } } diff --git a/nssa/core/src/lib.rs b/nssa/core/src/lib.rs index 478d475c..894b611f 100644 --- a/nssa/core/src/lib.rs +++ b/nssa/core/src/lib.rs @@ -3,13 +3,16 @@ 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, }; pub use encryption::{EncryptionScheme, SharedSecretKey}; pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; +pub use program::PrivateAccountKind; pub mod account; mod circuit_io; diff --git a/nssa/core/src/nullifier.rs b/nssa/core/src/nullifier.rs index aafe3f7c..ab23ddc0 100644 --- a/nssa/core/src/nullifier.rs +++ b/nssa/core/src/nullifier.rs @@ -12,10 +12,11 @@ pub type Identifier = u128; #[cfg_attr(any(feature = "host", test), derive(Hash))] pub struct NullifierPublicKey(pub [u8; 32]); -impl From<(&NullifierPublicKey, Identifier)> for AccountId { - fn from(value: (&NullifierPublicKey, Identifier)) -> Self { - let (npk, identifier) = value; - +impl AccountId { + /// Derives an [`AccountId`] for a regular (non-PDA) private account from the nullifier public + /// key and identifier. + #[must_use] + pub fn for_regular_private_account(npk: &NullifierPublicKey, identifier: Identifier) -> Self { // 32 bytes prefix || 32 bytes npk || 16 bytes identifier let mut bytes = [0; 80]; bytes[0..32].copy_from_slice(PRIVATE_ACCOUNT_ID_PREFIX); @@ -31,6 +32,12 @@ impl From<(&NullifierPublicKey, Identifier)> for AccountId { } } +impl From<(&NullifierPublicKey, Identifier)> for AccountId { + fn from((npk, identifier): (&NullifierPublicKey, Identifier)) -> Self { + Self::for_regular_private_account(npk, identifier) + } +} + impl AsRef<[u8]> for NullifierPublicKey { fn as_ref(&self) -> &[u8] { self.0.as_slice() @@ -155,7 +162,7 @@ mod tests { 253, 105, 164, 89, 84, 40, 191, 182, 119, 64, 255, 67, 142, ]); - let account_id = AccountId::from((&npk, 0)); + let account_id = AccountId::for_regular_private_account(&npk, 0); assert_eq!(account_id, expected_account_id); } @@ -172,7 +179,7 @@ mod tests { 56, 247, 99, 121, 165, 182, 234, 255, 19, 127, 191, 72, ]); - let account_id = AccountId::from((&npk, 1)); + let account_id = AccountId::for_regular_private_account(&npk, 1); assert_eq!(account_id, expected_account_id); } @@ -190,7 +197,7 @@ mod tests { 19, 245, 25, 214, 162, 209, 135, 252, 82, 27, 2, 174, 196, ]); - let account_id = AccountId::from((&npk, identifier)); + let account_id = AccountId::for_regular_private_account(&npk, identifier); assert_eq!(account_id, expected_account_id); } diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 1ef2ef6c..275b40a6 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,12 +1,11 @@ use std::collections::HashSet; -#[cfg(any(feature = "host", test))] use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; use crate::{ - BlockId, NullifierPublicKey, Timestamp, + BlockId, Identifier, NullifierPublicKey, Timestamp, account::{Account, AccountId, AccountWithMetadata}, }; @@ -27,7 +26,18 @@ pub struct ProgramInput { /// Each program can derive up to `2^256` unique account IDs by choosing different /// seeds. PDAs allow programs to control namespaced account identifiers without /// collisions between programs. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + Eq, + PartialEq, + Hash, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, +)] pub struct PdaSeed([u8; 32]); impl PdaSeed { @@ -35,6 +45,66 @@ impl PdaSeed { pub const fn new(value: [u8; 32]) -> Self { Self(value) } + + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl AsRef<[u8]> for PdaSeed { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// Discriminates the type of private account a ciphertext belongs to, carrying the data needed +/// to reconstruct the account's [`AccountId`] on the receiver side. +/// +/// [`AccountId`]: crate::account::AccountId +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +pub enum PrivateAccountKind { + Regular(Identifier), + Pda { + program_id: ProgramId, + seed: PdaSeed, + identifier: Identifier, + }, +} + +impl PrivateAccountKind { + /// Borsh layout (all integers little-endian, variant index is u8): + /// + /// ```text + /// Regular(ident): 0x00 || ident (16 LE) || [0u8; 64] + /// Pda { program_id, seed, ident }: 0x01 || program_id (32) || seed (32) || ident (16 LE) + /// ``` + /// + /// Both variants are zero-padded to the same length so all ciphertexts are the same size, + /// preventing observers from distinguishing `Regular` from `Pda` via ciphertext length. + /// `HEADER_LEN` equals the borsh size of the largest variant (`Pda`): 1 + 32 + 32 + 16 = 81. + pub const HEADER_LEN: usize = 81; + + #[must_use] + pub const fn identifier(&self) -> Identifier { + match self { + Self::Regular(identifier) | Self::Pda { identifier, .. } => *identifier, + } + } + + #[must_use] + pub fn to_header_bytes(&self) -> [u8; Self::HEADER_LEN] { + let mut bytes = [0_u8; Self::HEADER_LEN]; + let serialized = borsh::to_vec(self).expect("borsh serialization is infallible"); + bytes[..serialized.len()].copy_from_slice(&serialized); + bytes + } + + #[cfg(feature = "host")] + #[must_use] + pub fn from_header_bytes(bytes: &[u8; Self::HEADER_LEN]) -> Option { + BorshDeserialize::deserialize(&mut bytes.as_ref()).ok() + } } impl AccountId { @@ -59,27 +129,31 @@ impl AccountId { ) } - /// Derives an [`AccountId`] for a private PDA from the program ID, seed, and nullifier - /// public key. + /// Derives an [`AccountId`] for a private PDA from the program ID, seed, nullifier public + /// key, and identifier. /// /// Unlike public PDAs ([`AccountId::for_public_pda`]), this includes the `npk` in the /// derivation, making the address unique per group of controllers sharing viewing keys. + /// The `identifier` further diversifies the address, so a single `(program_id, seed, npk)` + /// tuple controls a family of 2^128 addresses. #[must_use] pub fn for_private_pda( program_id: &ProgramId, seed: &PdaSeed, npk: &NullifierPublicKey, + identifier: Identifier, ) -> Self { use risc0_zkvm::sha::{Impl, Sha256 as _}; const PRIVATE_PDA_PREFIX: &[u8; 32] = b"/LEE/v0.3/AccountId/PrivatePDA/\x00"; - let mut bytes = [0_u8; 128]; + let mut bytes = [0_u8; 144]; bytes[0..32].copy_from_slice(PRIVATE_PDA_PREFIX); let program_id_bytes: &[u8] = bytemuck::try_cast_slice(program_id).expect("ProgramId should be castable to &[u8]"); bytes[32..64].copy_from_slice(program_id_bytes); bytes[64..96].copy_from_slice(&seed.0); bytes[96..128].copy_from_slice(&npk.to_byte_array()); + bytes[128..144].copy_from_slice(&identifier.to_le_bytes()); Self::new( Impl::hash_bytes(&bytes) .as_bytes() @@ -87,6 +161,21 @@ impl AccountId { .expect("Hash output must be exactly 32 bytes long"), ) } + + /// Derives the [`AccountId`] for a private account from the nullifier public key and kind. + #[must_use] + pub fn for_private_account(npk: &NullifierPublicKey, kind: &PrivateAccountKind) -> Self { + match kind { + PrivateAccountKind::Regular(identifier) => { + Self::for_regular_private_account(npk, *identifier) + } + PrivateAccountKind::Pda { + program_id, + seed, + identifier, + } => Self::for_private_pda(program_id, seed, npk, *identifier), + } + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -845,19 +934,20 @@ mod tests { // ---- AccountId::for_private_pda tests ---- /// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific - /// `(program_id, seed, npk)` triple. Any change to `PRIVATE_PDA_PREFIX`, byte ordering, - /// or the underlying hash breaks this test. + /// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte + /// ordering, or the underlying hash breaks this test. #[test] fn for_private_pda_matches_pinned_value() { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); + let identifier: Identifier = u128::MAX; let expected = AccountId::new([ - 132, 198, 103, 173, 244, 211, 188, 217, 249, 99, 126, 205, 152, 120, 192, 47, 13, 53, - 133, 3, 17, 69, 92, 243, 140, 94, 182, 211, 218, 75, 215, 45, + 59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54, + 124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17, ]); assert_eq!( - AccountId::for_private_pda(&program_id, &seed, &npk), + AccountId::for_private_pda(&program_id, &seed, &npk, identifier), expected ); } @@ -870,8 +960,8 @@ mod tests { let npk_a = NullifierPublicKey([3; 32]); let npk_b = NullifierPublicKey([4; 32]); assert_ne!( - AccountId::for_private_pda(&program_id, &seed, &npk_a), - AccountId::for_private_pda(&program_id, &seed, &npk_b), + AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX), + AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX), ); } @@ -883,8 +973,8 @@ mod tests { let seed_b = PdaSeed::new([5; 32]); let npk = NullifierPublicKey([3; 32]); assert_ne!( - AccountId::for_private_pda(&program_id, &seed_a, &npk), - AccountId::for_private_pda(&program_id, &seed_b, &npk), + AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX), + AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX), ); } @@ -896,8 +986,25 @@ mod tests { let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); assert_ne!( - AccountId::for_private_pda(&program_id_a, &seed, &npk), - AccountId::for_private_pda(&program_id_b, &seed, &npk), + AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX), + AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX), + ); + } + + /// Different identifiers produce different addresses for the same `(program_id, seed, npk)`, + /// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses. + #[test] + fn for_private_pda_differs_for_different_identifier() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed, &npk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, 1), + ); + assert_ne!( + AccountId::for_private_pda(&program_id, &seed, &npk, 0), + AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX), ); } @@ -908,14 +1015,62 @@ mod tests { let program_id: ProgramId = [1; 8]; let seed = PdaSeed::new([2; 32]); let npk = NullifierPublicKey([3; 32]); - let private_id = AccountId::for_private_pda(&program_id, &seed, &npk); + let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX); let public_id = AccountId::for_public_pda(&program_id, &seed); assert_ne!(private_id, public_id); } - // ---- compute_public_authorized_pdas tests ---- + #[cfg(feature = "host")] + #[test] + fn private_account_kind_header_round_trips() { + let regular = PrivateAccountKind::Regular(42); + let pda = PrivateAccountKind::Pda { + program_id: [1_u32; 8], + seed: PdaSeed::new([2_u8; 32]), + identifier: u128::MAX, + }; + assert_eq!( + PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()), + Some(regular) + ); + assert_eq!( + PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()), + Some(pda) + ); + } + + #[cfg(feature = "host")] + #[test] + fn private_account_kind_unknown_discriminant_returns_none() { + let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN]; + bytes[0] = 0xFF; + assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None); + } + + #[test] + fn for_private_account_dispatches_correctly() { + let program_id: ProgramId = [1; 8]; + let seed = PdaSeed::new([2; 32]); + let npk = NullifierPublicKey([3; 32]); + let identifier: Identifier = 77; + + assert_eq!( + AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)), + AccountId::for_regular_private_account(&npk, identifier), + ); + assert_eq!( + AccountId::for_private_account( + &npk, + &PrivateAccountKind::Pda { + program_id, + seed, + identifier + } + ), + AccountId::for_private_pda(&program_id, &seed, &npk, identifier), + ); + } - /// `compute_public_authorized_pdas` returns the public PDA addresses for the caller's seeds. #[test] fn compute_public_authorized_pdas_with_seeds() { let caller: ProgramId = [1; 8]; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index f5bd8cea..09e37664 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,8 +176,10 @@ 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, + PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, + program::{PdaSeed, PrivateAccountKind}, }; use super::*; @@ -199,6 +193,21 @@ mod tests { }, }; + fn decrypt_kind( + output: &PrivacyPreservingCircuitOutput, + ssk: &SharedSecretKey, + idx: usize, + ) -> PrivateAccountKind { + let (kind, _) = EncryptionScheme::decrypt( + &output.ciphertexts[idx], + ssk, + &output.new_commitments[idx], + u32::try_from(idx).expect("idx fits in u32"), + ) + .unwrap(); + kind + } + #[test] fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { let recipient_keys = test_private_account_keys_1(); @@ -213,7 +222,7 @@ mod tests { AccountId::new([0; 32]), ); - let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0)); + let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); let balance_to_move: u128 = 37; @@ -240,10 +249,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(); @@ -283,12 +296,12 @@ mod tests { data: Data::default(), }, true, - AccountId::from((&sender_keys.npk(), 0)), + AccountId::for_regular_private_account(&sender_keys.npk(), 0), ); - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account); - let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0)); + let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id); let balance_to_move: u128 = 37; @@ -333,13 +346,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(); @@ -376,7 +397,7 @@ mod tests { let pre = AccountWithMetadata::new( Account::default(), false, - AccountId::from((&account_keys.npk(), 0)), + AccountId::for_regular_private_account(&account_keys.npk(), 0), ); let validity_window_chain_caller = Program::validity_window_chain_caller(); @@ -402,10 +423,431 @@ 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, + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + /// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. + #[test] + fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() { + let program = Program::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let (output, _proof) = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier, + }], + &program.clone().into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &shared_secret, 0), + PrivateAccountKind::Pda { + program_id: program.id(), + seed, + identifier + }, + ); + } + + /// PDA init: initializes a new PDA under `authenticated_transfer`'s ownership. + /// The `auth_transfer_proxy` program chains to `authenticated_transfer` with `pda_seeds` + /// to establish authorization and the private PDA binding. + #[test] + fn private_pda_init() { + let program = Program::auth_transfer_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + 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); + + let auth_id = auth_transfer.id(); + let program_with_deps = + ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into()); + + // is_withdraw=false triggers init path (1 pre-state) + let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap(); + + let result = execute_and_prove( + vec![pda_pre], + instruction, + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + identifier: 0, + }], + &program_with_deps, + ); + + let (output, _proof) = result.expect("PDA init should succeed"); + assert_eq!(output.new_commitments.len(), 1); + } + + /// PDA withdraw: chains to `authenticated_transfer` to move balance from PDA to recipient. + /// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a + /// two-tx sequence with membership proofs. + #[test] + fn private_pda_withdraw() { + let program = Program::auth_transfer_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + 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, private PDA) + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); + let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id); + + // Recipient (public) + let recipient_id = AccountId::new([88; 32]); + let recipient_pre = AccountWithMetadata::new( + Account { + program_owner: auth_transfer.id(), + balance: 10000, + ..Account::default() + }, + true, + recipient_id, + ); + + let auth_id = auth_transfer.id(); + let program_with_deps = + ProgramWithDependencies::new(program, [(auth_id, auth_transfer)].into()); + + // is_withdraw=true, amount=0 (PDA has no balance yet) + let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap(); + + let result = execute_and_prove( + vec![pda_pre, recipient_pre], + instruction, + vec![ + InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret_pda, + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ); + + let (output, _proof) = result.expect("PDA withdraw should succeed"); + assert_eq!(output.new_commitments.len(), 1); + } + + /// Shared regular private account: receives funds via `authenticated_transfer` directly, + /// no custom program needed. This demonstrates the non-PDA shared account flow where + /// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account + /// uses the standard unauthorized private account path and works with auth-transfer's + /// transfer path like any other private account. + #[test] + fn shared_account_receives_via_auth_transfer() { + let program = Program::authenticated_transfer_program(); + let shared_keys = test_private_account_keys_1(); + let shared_npk = shared_keys.npk(); + let shared_identifier: u128 = 42; + let shared_secret = SharedSecretKey::new(&[55; 32], &shared_keys.vpk()); + + // Sender: public account with balance, owned by auth-transfer + let sender_id = AccountId::new([99; 32]); + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 1000, + ..Account::default() + }, + true, + sender_id, + ); + + // Recipient: shared private account (new, unauthorized) + let shared_account_id = AccountId::from((&shared_npk, shared_identifier)); + let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id); + + let balance_to_move: u128 = 100; + let instruction = Program::serialize_instruction(balance_to_move).unwrap(); + + let result = execute_and_prove( + vec![sender, recipient], + instruction, + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: shared_npk, + ssk: shared_secret, + identifier: shared_identifier, + }, + ], + &program.into(), + ); + + let (output, _proof) = result.expect("shared account receive should succeed"); + // Sender is public (no commitment), recipient is private (1 commitment) + assert_eq!(output.new_commitments.len(), 1); + } + + /// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_authorized_init_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let pre = AccountWithMetadata::new(Account::default(), true, account_id); + + let (output, _) = execute_and_prove( + vec![pre], + Program::serialize_instruction(0_u128).unwrap(), + vec![InputAccountIdentity::PrivateAuthorizedInit { + ssk, + nsk: keys.nsk, + identifier, + }], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); + } + + /// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_unauthorized_init_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let sender = AccountWithMetadata::new( + Account { + program_owner: program.id(), + balance: 1, + ..Account::default() + }, + true, + AccountId::new([0; 32]), + ); + let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id); + + let (output, _) = execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(1_u128).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivateUnauthorized { + npk: keys.npk(), + ssk, + identifier, + }, + ], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); + } + + /// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Regular` carrying the correct identifier. + #[test] + fn private_authorized_update_encrypts_regular_kind_with_identifier() { + let program = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); + let account = Account { + program_owner: program.id(), + balance: 1, + ..Account::default() + }; + let commitment = Commitment::new(&account_id, &account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&commitment)); + + let sender = AccountWithMetadata::new(account, true, account_id); + let recipient = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let (output, _) = execute_and_prove( + vec![sender, recipient], + Program::serialize_instruction(1_u128).unwrap(), + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), + identifier, + }, + InputAccountIdentity::Public, + ], + &program.into(), + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Regular(identifier) + ); + } + + /// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts + /// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`. + #[test] + fn private_pda_update_encrypts_pda_kind_with_identifier() { + let program = Program::pda_fund_spend_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let identifier: u128 = 99; + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let auth_transfer_id = auth_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); + let pda_account = Account { + program_owner: auth_transfer_id, + balance: 1, + ..Account::default() + }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = + AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = ProgramWithDependencies::new( + program.clone(), + [(auth_transfer_id, auth_transfer)].into(), + ); + + let (output, _) = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + identifier, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + + assert_eq!( + decrypt_kind(&output, &ssk, 0), + PrivateAccountKind::Pda { + program_id: program.id(), + seed, + identifier + }, + ); + } + + #[test] + fn private_pda_init_identifier_mismatch_fails() { + let program = Program::pda_claimer(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); + let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); + + let result = execute_and_prove( + vec![pre_state], + Program::serialize_instruction(seed).unwrap(), + vec![InputAccountIdentity::PrivatePdaInit { + npk, + ssk: shared_secret, + identifier: 99, + }], + &program.into(), + ); + + assert!(matches!(result, Err(NssaError::CircuitProvingError(_)))); + } + + #[test] + fn private_pda_update_identifier_mismatch_fails() { + let program = Program::pda_fund_spend_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + let keys = test_private_account_keys_1(); + let npk = keys.npk(); + let seed = PdaSeed::new([42; 32]); + let ssk = SharedSecretKey::new(&[55; 32], &keys.vpk()); + + let auth_transfer_id = auth_transfer.id(); + let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); + let pda_account = Account { + program_owner: auth_transfer_id, + balance: 1, + ..Account::default() + }; + let pda_commitment = Commitment::new(&pda_id, &pda_account); + let mut commitment_set = CommitmentSet::with_capacity(1); + commitment_set.extend(std::slice::from_ref(&pda_commitment)); + + let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id); + let recipient_pre = + AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32])); + + let program_with_deps = + ProgramWithDependencies::new(program, [(auth_transfer_id, auth_transfer)].into()); + + let result = execute_and_prove( + vec![pda_pre, recipient_pre], + Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk, + nsk: keys.nsk, + membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), + identifier: 99, + }, + InputAccountIdentity::Public, + ], &program_with_deps, ); diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 01e6e04f..d86273d8 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -122,15 +122,16 @@ impl Message { } #[must_use] - pub fn hash_message(&self) -> [u8; 32] { + pub fn hash(&self) -> [u8; 32] { + let msg = self.to_bytes(); let mut bytes = Vec::with_capacity( PREFIX .len() - .checked_add(self.to_bytes().len()) + .checked_add(msg.len()) .expect("length overflow"), ); bytes.extend_from_slice(PREFIX); - bytes.extend_from_slice(&self.to_bytes()); + bytes.extend_from_slice(&msg); Sha256::digest(bytes).into() } @@ -139,7 +140,8 @@ impl Message { #[cfg(test)] pub mod tests { use nssa_core::{ - Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, SharedSecretKey, + Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind, + SharedSecretKey, account::{Account, AccountId, Nonce}, encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{BlockValidityWindow, TimestampValidityWindow}, @@ -167,10 +169,10 @@ pub mod tests { let encrypted_private_post_states = Vec::new(); - let account_id2 = nssa_core::account::AccountId::from((&npk2, 0)); + let account_id2 = nssa_core::account::AccountId::for_regular_private_account(&npk2, 0); let new_commitments = vec![Commitment::new(&account_id2, &account2)]; - let account_id1 = nssa_core::account::AccountId::from((&npk1, 0)); + let account_id1 = nssa_core::account::AccountId::for_regular_private_account(&npk1, 0); let old_commitment = Commitment::new(&account_id1, &account1); let new_nullifiers = vec![( Nullifier::for_account_update(&old_commitment, &nsk1), @@ -190,7 +192,7 @@ pub mod tests { } #[test] - fn hash_message_privacy_pinned() { + fn hash_privacy_pinned() { let msg = Message { public_account_ids: vec![AccountId::new([42_u8; 32])], nonces: vec![Nonce(5)], @@ -206,7 +208,7 @@ pub mod tests { let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // all remaining vec fields are empty: u32 len=0 let empty_vec_bytes: &[u8] = &[0_u8; 4]; - // validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} + // validity windows: unbounded = {from: None (0u8), to: None (0u8)} let unbounded_window_bytes: &[u8] = &[0_u8; 2]; let expected_borsh_vec: Vec = [ @@ -226,7 +228,7 @@ pub mod tests { assert_eq!( borsh::to_vec(&msg).unwrap(), expected_borsh, - "`privacy_preserving_transaction::hash_message()`: expected borsh order has changed" + "`privacy_preserving_transaction::hash()`: expected borsh order has changed" ); let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len()); @@ -235,9 +237,9 @@ pub mod tests { let expected_hash: [u8; 32] = Sha256::digest(&preimage).into(); assert_eq!( - msg.hash_message(), + msg.hash(), expected_hash, - "`privacy_preserving_transaction::hash_message()`: serialization has changed" + "`privacy_preserving_transaction::hash()`: serialization has changed" ); } @@ -246,12 +248,18 @@ pub mod tests { let npk = NullifierPublicKey::from(&[1; 32]); let vpk = ViewingPublicKey::from_scalar([2; 32]); let account = Account::default(); - let account_id = nssa_core::account::AccountId::from((&npk, 0)); + let account_id = nssa_core::account::AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); let esk = [3; 32]; let shared_secret = SharedSecretKey::new(&esk, &vpk); let epk = EphemeralPublicKey::from_scalar(esk); - let ciphertext = EncryptionScheme::encrypt(&account, 0, &shared_secret, &commitment, 2); + let ciphertext = EncryptionScheme::encrypt( + &account, + &PrivateAccountKind::Regular(0), + &shared_secret, + &commitment, + 2, + ); let encrypted_account_data = EncryptedAccountData::new(ciphertext.clone(), &npk, &vpk, epk.clone()); diff --git a/nssa/src/privacy_preserving_transaction/witness_set.rs b/nssa/src/privacy_preserving_transaction/witness_set.rs index 43a36671..20f4c7ca 100644 --- a/nssa/src/privacy_preserving_transaction/witness_set.rs +++ b/nssa/src/privacy_preserving_transaction/witness_set.rs @@ -15,7 +15,7 @@ impl WitnessSet { #[must_use] // TODO: swap for Keycard signing path. pub fn for_message(message: &Message, proof: Proof, private_keys: &[&PrivateKey]) -> Self { - let message_hash = message.hash_message(); + let message_hash = message.hash(); let signatures_and_public_keys = private_keys .iter() .map(|&key| { @@ -49,7 +49,7 @@ impl WitnessSet { #[must_use] pub fn signatures_are_valid_for(&self, message: &Message) -> bool { - let message_hash = message.hash_message(); + let message_hash = message.hash(); for (signature, public_key) in self.signatures_and_public_keys() { if !signature.is_valid_for(&message_hash, public_key) { return false; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b8c3fe77..059aa5ca 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -312,6 +312,16 @@ mod tests { } } + #[must_use] + pub fn auth_transfer_proxy() -> Self { + use test_program_methods::{AUTH_TRANSFER_PROXY_ELF, AUTH_TRANSFER_PROXY_ID}; + + Self { + id: AUTH_TRANSFER_PROXY_ID, + elf: AUTH_TRANSFER_PROXY_ELF.to_vec(), + } + } + #[must_use] pub fn two_pda_claimer() -> Self { use test_program_methods::{TWO_PDA_CLAIMER_ELF, TWO_PDA_CLAIMER_ID}; @@ -322,6 +332,16 @@ mod tests { } } + #[must_use] + pub fn pda_fund_spend_proxy() -> Self { + use test_program_methods::{PDA_FUND_SPEND_PROXY_ELF, PDA_FUND_SPEND_PROXY_ID}; + + Self { + id: PDA_FUND_SPEND_PROXY_ID, + elf: PDA_FUND_SPEND_PROXY_ELF.to_vec(), + } + } + #[must_use] pub fn changer_claimer() -> Self { use test_program_methods::{CHANGER_CLAIMER_ELF, CHANGER_CLAIMER_ID}; diff --git a/nssa/src/public_transaction/message.rs b/nssa/src/public_transaction/message.rs index f71fb372..3ab7d74c 100644 --- a/nssa/src/public_transaction/message.rs +++ b/nssa/src/public_transaction/message.rs @@ -68,7 +68,7 @@ impl Message { } #[must_use] - pub fn hash_message(&self) -> [u8; 32] { + pub fn hash(&self) -> [u8; 32] { let mut bytes = Vec::with_capacity( PREFIX .len() @@ -90,7 +90,7 @@ mod tests { use super::{Message, PREFIX}; #[test] - fn hash_message_public_pinned() { + fn hash_public_pinned() { let msg = Message::new_preserialized( [1_u32; 8], vec![AccountId::new([42_u8; 32])], @@ -122,7 +122,7 @@ mod tests { assert_eq!( borsh::to_vec(&msg).unwrap(), expected_borsh, - "`public_transaction::hash_message()`: expected borsh order has changed" + "`public_transaction::hash()`: expected borsh order has changed" ); let mut preimage = Vec::with_capacity(PREFIX.len() + expected_borsh.len()); @@ -131,9 +131,9 @@ mod tests { let expected_hash: [u8; 32] = Sha256::digest(&preimage).into(); assert_eq!( - msg.hash_message(), + msg.hash(), expected_hash, - "`public_transaction::hash_message()`: serialization has changed" + "`public_transaction::hash()`: serialization has changed" ); } } diff --git a/nssa/src/public_transaction/witness_set.rs b/nssa/src/public_transaction/witness_set.rs index 7a32c0ea..f5721091 100644 --- a/nssa/src/public_transaction/witness_set.rs +++ b/nssa/src/public_transaction/witness_set.rs @@ -41,7 +41,7 @@ impl WitnessSet { #[must_use] pub fn for_message(message: &Message, private_keys: &[&PrivateKey]) -> Self { - let message_hash = message.hash_message(); + let message_hash = message.hash(); let signatures_and_public_keys = private_keys .iter() .map(|&key| { @@ -58,7 +58,7 @@ impl WitnessSet { #[must_use] pub fn is_valid_for(&self, message: &Message) -> bool { - let message_hash = message.hash_message(); + let message_hash = message.hash(); for (signature, public_key) in self.signatures_and_public_keys() { if !signature.is_valid_for(&message_hash, public_key) { return false; @@ -173,7 +173,7 @@ mod tests { assert_eq!(witness_set.signatures_and_public_keys.len(), 2); - let message_bytes = message.hash_message(); + let message_bytes = message.hash(); for ((signature, public_key), expected_public_key) in witness_set .signatures_and_public_keys .into_iter() diff --git a/nssa/src/signature/mod.rs b/nssa/src/signature/mod.rs index 19daca2e..a46b1ff5 100644 --- a/nssa/src/signature/mod.rs +++ b/nssa/src/signature/mod.rs @@ -36,9 +36,9 @@ impl FromStr for Signature { } impl Signature { - #[must_use] /// This function expects the incoming message to be prehashed to be pre-2022 BIP-340/Keycard /// compatible. + #[must_use] pub fn new(key: &PrivateKey, message: &[u8; 32]) -> Self { let mut aux_random = [0_u8; 32]; OsRng.fill_bytes(&mut aux_random); diff --git a/nssa/src/state.rs b/nssa/src/state.rs index ff16175c..8cf1d68e 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::{ @@ -459,7 +459,7 @@ pub mod tests { #[must_use] pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self { - let account_id = AccountId::from((&keys.npk(), 0)); + let account_id = AccountId::for_regular_private_account(&keys.npk(), 0); let commitment = Commitment::new(&account_id, account); self.private_state.0.extend(&[commitment]); self @@ -618,8 +618,8 @@ pub mod tests { ..Account::default() }; - let account_id1 = AccountId::from((&keys1.npk(), 0)); - let account_id2 = AccountId::from((&keys2.npk(), 0)); + let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), 0); + let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), 0); let init_commitment1 = Commitment::new(&account_id1, &account); let init_commitment2 = Commitment::new(&account_id2, &account); @@ -1256,6 +1256,12 @@ pub mod tests { } } + fn test_public_account_keys_2() -> TestPublicKeys { + TestPublicKeys { + signing_key: PrivateKey::try_new([38; 32]).unwrap(), + } + } + pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { nsk: [13; 32], @@ -1294,10 +1300,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(); @@ -1322,7 +1332,7 @@ pub mod tests { state: &V03State, ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); let sender_pre = AccountWithMetadata::new( sender_private_account.clone(), @@ -1343,13 +1353,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(); @@ -1378,7 +1396,7 @@ pub mod tests { state: &V03State, ) -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); let sender_commitment = Commitment::new(&sender_account_id, sender_private_account); let sender_pre = AccountWithMetadata::new( sender_private_account.clone(), @@ -1398,10 +1416,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(); @@ -1486,8 +1511,8 @@ pub mod tests { &state, ); - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); - let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); + let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); let expected_new_commitment_1 = Commitment::new( &sender_account_id, &Account { @@ -1565,7 +1590,7 @@ pub mod tests { &state, ); - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); let expected_new_commitment = Commitment::new( &sender_account_id, &Account { @@ -1615,10 +1640,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 +1663,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 +1686,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 +1709,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 +1740,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 +1763,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 +1795,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 +1818,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 +1850,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 +1879,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 +1907,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 +1960,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 +2006,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 +2052,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 +2098,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 +2142,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 +2183,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(), ); @@ -2395,16 +2213,17 @@ pub mod tests { let seed = PdaSeed::new([42; 32]); let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); 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(), ); @@ -2433,16 +2252,17 @@ pub mod tests { // `account_id` is derived from `npk_a`, but `npk_b` is supplied for this pre_state. // `AccountId::for_private_pda(program, seed, npk_b) != account_id`, so the claim check in // the circuit must reject. - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk_a, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); 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(), ); @@ -2463,7 +2283,7 @@ pub mod tests { let seed = PdaSeed::new([77; 32]); let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); - let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk); + let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let callee_id = callee.id(); @@ -2473,10 +2293,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, ); @@ -2500,7 +2321,7 @@ pub mod tests { let wrong_delegated_seed = PdaSeed::new([88; 32]); let shared_secret = SharedSecretKey::new(&[55; 32], &keys.vpk()); - let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk); + let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); let callee_id = callee.id(); @@ -2510,10 +2331,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, ); @@ -2537,8 +2359,8 @@ pub mod tests { let shared_a = SharedSecretKey::new(&[66; 32], &keys_a.vpk()); let shared_b = SharedSecretKey::new(&[77; 32], &keys_b.vpk()); - let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk()); - let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk()); + let account_a = AccountId::for_private_pda(&program.id(), &seed, &keys_a.npk(), u128::MAX); + let account_b = AccountId::for_private_pda(&program.id(), &seed, &keys_b.npk(), u128::MAX); let pre_a = AccountWithMetadata::new(Account::default(), false, account_a); let pre_b = AccountWithMetadata::new(Account::default(), false, account_b); @@ -2546,10 +2368,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(), ); @@ -2577,7 +2407,7 @@ pub mod tests { // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = // true, account_id derived via the private formula. - let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk); + let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); let owned_pre_state = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2590,146 +2420,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 +2501,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 +2809,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(), ); @@ -3130,7 +2826,7 @@ pub mod tests { balance: 100, ..Account::default() }; - let sender_account_id = AccountId::from((&sender_keys.npk(), 0)); + let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0); let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id); let mut state = V03State::new_with_genesis_accounts( @@ -3152,10 +2848,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(); @@ -3216,8 +2919,8 @@ pub mod tests { (&to_keys.npk(), 0), ); - let from_account_id = AccountId::from((&from_keys.npk(), 0)); - let to_account_id = AccountId::from((&to_keys.npk(), 0)); + let from_account_id = AccountId::for_regular_private_account(&from_keys.npk(), 0); + let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), 0); let from_commitment = Commitment::new(&from_account_id, &from_account.account); let to_commitment = Commitment::new(&to_account_id, &to_account.account); let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id); @@ -3273,12 +2976,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 +3256,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(); @@ -3565,7 +3280,7 @@ pub mod tests { let result = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); assert!(result.is_ok()); - let account_id = AccountId::from((&private_keys.npk(), 0)); + let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); } @@ -3590,10 +3305,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(); @@ -3613,7 +3329,7 @@ pub mod tests { .transition_from_privacy_preserving_transaction(&tx, 1, 0) .unwrap(); - let account_id = AccountId::from((&private_keys.npk(), 0)); + let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); } @@ -3642,10 +3358,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(); @@ -3669,7 +3386,7 @@ pub mod tests { ); // Verify the account is now initialized (nullifier exists) - let account_id = AccountId::from((&private_keys.npk(), 0)); + let account_id = AccountId::for_regular_private_account(&private_keys.npk(), 0); let nullifier = Nullifier::for_account_initialization(&account_id); assert!(state.private_state.1.contains(&nullifier)); @@ -3688,10 +3405,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 +3482,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 +3508,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(), ); @@ -3827,7 +3541,7 @@ pub mod tests { let recipient_account = AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0)); - let recipient_account_id = AccountId::from((&recipient_keys.npk(), 0)); + let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0); let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account); let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id); @@ -3852,10 +3566,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 +3723,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 +3793,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(); @@ -4577,4 +4300,225 @@ pub mod tests { "program with spoofed caller_program_id in output should be rejected" ); } + + #[test] + fn two_private_pda_family_members_receive_and_spend() { + let funder_keys = test_public_account_keys_1(); + let alice_keys = test_private_account_keys_1(); + let alice_npk = alice_keys.npk(); + + let proxy = Program::pda_fund_spend_proxy(); + let auth_transfer = Program::authenticated_transfer_program(); + let proxy_id = proxy.id(); + let auth_transfer_id = auth_transfer.id(); + let seed = PdaSeed::new([42; 32]); + let amount: u128 = 100; + + let program_with_deps = + ProgramWithDependencies::new(proxy, [(auth_transfer_id, auth_transfer)].into()); + + let funder_id = funder_keys.account_id(); + let alice_pda_0_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 0); + let alice_pda_1_id = AccountId::for_private_pda(&proxy_id, &seed, &alice_npk, 1); + let recipient_id = test_public_account_keys_2().account_id(); + let recipient_signing_key = test_public_account_keys_2().signing_key; + + let mut state = V03State::new_with_genesis_accounts(&[(funder_id, 500)], vec![], 0); + + let alice_pda_0_account = Account { + program_owner: auth_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_0_id), + ..Account::default() + }; + let alice_pda_1_account = Account { + program_owner: auth_transfer_id, + balance: amount, + nonce: Nonce::private_account_nonce_init(&alice_pda_1_id), + ..Account::default() + }; + + let alice_shared_0 = SharedSecretKey::new(&[10; 32], &alice_keys.vpk()); + let alice_shared_1 = SharedSecretKey::new(&[11; 32], &alice_keys.vpk()); + + // Fund alice_pda_0 + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_0_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_0, + identifier: 0, + }, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![funder_id], + vec![funder_nonce], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([10; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 1, + 0, + ) + .unwrap(); + } + + // Fund alice_pda_1 + { + let funder_account = state.get_account_by_id(funder_id); + let funder_nonce = funder_account.nonce; + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(funder_account, true, funder_id), + AccountWithMetadata::new(Account::default(), false, alice_pda_1_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, true)).unwrap(), + vec![ + InputAccountIdentity::Public, + InputAccountIdentity::PrivatePdaInit { + npk: alice_npk, + ssk: alice_shared_1, + identifier: 1, + }, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![funder_id], + vec![funder_nonce], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([11; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 2, + 0, + ) + .unwrap(); + } + + let commitment_pda_0 = Commitment::new(&alice_pda_0_id, &alice_pda_0_account); + let commitment_pda_1 = Commitment::new(&alice_pda_1_id, &alice_pda_1_account); + + assert!(state.get_proof_for_commitment(&commitment_pda_0).is_some()); + assert!(state.get_proof_for_commitment(&commitment_pda_1).is_some()); + + // Alice spends alice_pda_0 into the public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_0_account, true, alice_pda_0_id), + AccountWithMetadata::new(recipient_account, true, recipient_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_0, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_0) + .expect("pda_0 must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![Nonce(0)], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([10; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 3, + 0, + ) + .unwrap(); + } + + // Alice spends alice_pda_1 into the same public recipient. + { + let recipient_account = state.get_account_by_id(recipient_id); + let (output, proof) = execute_and_prove( + vec![ + AccountWithMetadata::new(alice_pda_1_account, true, alice_pda_1_id), + AccountWithMetadata::new(recipient_account, false, recipient_id), + ], + Program::serialize_instruction((seed, amount, auth_transfer_id, false)).unwrap(), + vec![ + InputAccountIdentity::PrivatePdaUpdate { + ssk: alice_shared_1, + nsk: alice_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&commitment_pda_1) + .expect("pda_1 must be in state"), + identifier: 1, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .unwrap(); + let message = Message::try_from_circuit_output( + vec![recipient_id], + vec![], + vec![( + alice_npk, + alice_keys.vpk(), + EphemeralPublicKey::from_scalar([11; 32]), + )], + output, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + state + .transition_from_privacy_preserving_transaction( + &PrivacyPreservingTransaction::new(message, witness_set), + 4, + 0, + ) + .unwrap(); + } + + assert_eq!(state.get_account_by_id(recipient_id).balance, 2 * amount); + } } diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit.rs b/program_methods/guest/src/bin/privacy_preserving_circuit.rs index 70979b7e..16ad56d2 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit.rs @@ -1,12 +1,13 @@ use std::{ - collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + collections::{HashMap, VecDeque, hash_map::Entry}, convert::Infallible, }; use nssa_core::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, Identifier, - MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, SharedSecretKey, + InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, + PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput, PrivateAccountKind, + SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce}, compute_digest_for_path, program::{ @@ -17,25 +18,26 @@ use nssa_core::{ }; use risc0_zkvm::{guest::env, serde::to_vec}; -const PRIVATE_PDA_FIXED_IDENTIFIER: u128 = u128::MAX; - /// 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 - /// their `AccountId` via a proven `AccountId::for_private_pda(program_id, seed, npk)` - /// check. + /// 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. - private_pda_bound_positions: HashSet, + /// 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 /// one private PDA per distinct npk. Without this check, a single `pda_seeds: [S]` entry in @@ -45,39 +47,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 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. - private_pda_npk_by_position: HashMap, + /// 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, _, _) = 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); - } - } + // 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)); } } @@ -115,7 +108,7 @@ impl ExecutionState { post_states: HashMap::new(), block_validity_window, timestamp_validity_window, - private_pda_bound_positions: HashSet::new(), + private_pda_bound_positions: HashMap::new(), pda_family_binding: HashMap::new(), private_pda_npk_by_position, }; @@ -194,7 +187,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,14 +204,16 @@ 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(&pos), + 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 +246,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 +324,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,40 +348,46 @@ impl ExecutionState { pre_account_id, ); } - }, - 3 => { - match claim { - Claim::Authorized => { - assert!( - pre_is_authorized, - "Cannot claim unauthorized private PDA {pre_account_id}" - ); - } - Claim::Pda(seed) => { - let npk = 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); - 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); - 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}" + ); + bind_private_pda_position( + &mut self.private_pda_bound_positions, + 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; @@ -438,6 +439,24 @@ fn assert_family_binding( } } +fn bind_private_pda_position( + map: &mut HashMap, + position: usize, + program_id: ProgramId, + seed: PdaSeed, +) { + match map.entry(position) { + Entry::Occupied(e) => assert_eq!( + *e.get(), + (program_id, seed), + "Duplicate binding at position {position}: conflicting (program_id, seed)" + ), + Entry::Vacant(e) => { + e.insert((program_id, seed)); + } + } +} + /// Resolve the authorization state of a `pre_state` seen again in a chained call and record /// any resulting bindings. Returns `true` if the `pre_state` is authorized through either a /// previously-seen authorization or a matching caller seed (under the public or private @@ -453,8 +472,8 @@ fn assert_family_binding( )] fn resolve_authorization_and_record_bindings( pda_family_binding: &mut HashMap<(ProgramId, PdaSeed), AccountId>, - private_pda_bound_positions: &mut HashSet, - private_pda_npk_by_position: &HashMap, + private_pda_bound_positions: &mut HashMap, + private_pda_npk_by_position: &HashMap, pre_account_id: AccountId, pre_state_position: usize, caller_program_id: Option, @@ -467,8 +486,9 @@ fn resolve_authorization_and_record_bindings( if AccountId::for_public_pda(&caller, seed) == pre_account_id { return Some((*seed, false, caller)); } - if let Some(npk) = private_pda_npk_by_position.get(&pre_state_position) - && AccountId::for_private_pda(&caller, seed, npk) == pre_account_id + if let Some((npk, identifier)) = + private_pda_npk_by_position.get(&pre_state_position) + && AccountId::for_private_pda(&caller, seed, npk, *identifier) == pre_account_id { return Some((*seed, true, caller)); } @@ -479,7 +499,12 @@ fn resolve_authorization_and_record_bindings( if let Some((seed, is_private_form, caller)) = matched_caller_seed { assert_family_binding(pda_family_binding, caller, seed, pre_account_id); if is_private_form { - private_pda_bound_positions.insert(pre_state_position); + bind_private_pda_position( + private_pda_bound_positions, + pre_state_position, + caller, + seed, + ); } } @@ -487,11 +512,8 @@ fn resolve_authorization_and_record_bindings( } fn compute_circuit_output( - execution_state: ExecutionState, - visibility_mask: &[u8], - private_account_keys: &[(NullifierPublicKey, Identifier, SharedSecretKey)], - private_account_nsks: &[NullifierSecretKey], - private_account_membership_proofs: &[Option], + mut execution_state: ExecutionState, + account_identities: &[InputAccountIdentity], ) -> PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput { public_pre_states: Vec::new(), @@ -503,292 +525,280 @@ fn compute_circuit_output( timestamp_validity_window: execution_state.timestamp_validity_window, }; + 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 (account_visibility_mask, (pre_state, post_state)) in - visibility_mask.iter().copied().zip(states_iter) + 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"); - }; - assert_ne!( - *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, - "Identifier must be different from {PRIVATE_PDA_FIXED_IDENTIFIER}. This is reserved for private PDA." - ); - - let account_id = AccountId::from((npk, *identifier)); + InputAccountIdentity::PrivateAuthorizedInit { + ssk, + nsk, + identifier, + } => { + let npk = NullifierPublicKey::from(nsk); + let account_id = AccountId::for_regular_private_account(&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, - *identifier, - shared_secret, - &commitment_post, - output_index, + assert!( + pre_state.is_authorized, + "Pre-state not authorized for authenticated private account" ); - - 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")); - } - 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) == 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"); - }; - assert_eq!( - *identifier, PRIVATE_PDA_FIXED_IDENTIFIER, - "Identifier for private PDAs must be {PRIVATE_PDA_FIXED_IDENTIFIER}." + pre_state.account, + Account::default(), + "Found new private account with non default values" ); - 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 encrypted_account = EncryptionScheme::encrypt( - &post_with_updated_nonce, - PRIVATE_PDA_FIXED_IDENTIFIER, - shared_secret, - &commitment_post, - output_index, + let new_nullifier = ( + Nullifier::for_account_initialization(&account_id), + DUMMY_COMMITMENT_HASH, ); + let new_nonce = pre_state.account.nonce.private_account_nonce_increment(nsk); - 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")); + 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::for_regular_private_account(&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::for_regular_private_account(npk, *identifier); + + assert_eq!(account_id, pre_state.account_id, "AccountId mismatch"); + 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 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, + ); + } + 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 = ( + 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("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, + }, + 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" + ); + + 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/lib.rs b/sequencer/core/src/lib.rs index 22c09d85..df9aa87c 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."); @@ -110,7 +141,7 @@ impl SequencerCore 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/auth_transfer_proxy.rs b/test_program_methods/guest/src/bin/auth_transfer_proxy.rs new file mode 100644 index 00000000..17316f16 --- /dev/null +++ b/test_program_methods/guest/src/bin/auth_transfer_proxy.rs @@ -0,0 +1,97 @@ +use nssa_core::program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, +}; + +/// PDA authorization program that delegates balance operations to `authenticated_transfer`. +/// +/// The PDA is owned by `authenticated_transfer`, not by this program. This program's role +/// is solely to provide PDA authorization via `pda_seeds` in chained calls. +/// +/// Instruction: `(pda_seed, auth_transfer_id, amount, is_withdraw)`. +/// +/// **Init** (`is_withdraw = false`, 1 pre-state `[pda]`): +/// Chains to `authenticated_transfer` with `instruction=0` (init path) and `pda_seeds=[seed]` +/// to initialize the PDA under `authenticated_transfer`'s ownership. +/// +/// **Withdraw** (`is_withdraw = true`, 2 pre-states `[pda, recipient]`): +/// Chains to `authenticated_transfer` with the amount and `pda_seeds=[seed]` to authorize +/// the PDA for a balance transfer. The actual balance modification happens in +/// `authenticated_transfer`, not here. +/// +/// **Deposit**: done directly via `authenticated_transfer` (no need for this program). +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, auth_transfer_id, amount, is_withdraw), + }, + instruction_words, + ) = read_nssa_inputs::(); + + if is_withdraw { + let Ok([pda_pre, recipient_pre]) = <[_; 2]>::try_from(pre_states.clone()) else { + panic!("expected exactly 2 pre_states for withdraw: [pda, recipient]"); + }; + + // Post-states stay unchanged in this program. The actual balance transfer + // happens in the chained call to authenticated_transfer. + let pda_post = AccountPostState::new(pda_pre.account.clone()); + let recipient_post = AccountPostState::new(recipient_pre.account.clone()); + + // Chain to authenticated_transfer with pda_seeds to authorize the PDA. + // The circuit's resolve_authorization_and_record_bindings establishes the + // private PDA (seed, npk) binding when pda_seeds match the private PDA derivation. + let mut auth_pda_pre = pda_pre; + auth_pda_pre.is_authorized = true; + let auth_call = + ChainedCall::new(auth_transfer_id, vec![auth_pda_pre, recipient_pre], &amount) + .with_pda_seeds(vec![pda_seed]); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + vec![pda_post, recipient_post], + ) + .with_chained_calls(vec![auth_call]) + .write(); + } else { + // Init: initialize the PDA under authenticated_transfer's ownership. + let Ok([pda_pre]) = <[_; 1]>::try_from(pre_states.clone()) else { + panic!("expected exactly 1 pre_state for init: [pda]"); + }; + + let pda_post = AccountPostState::new(pda_pre.account.clone()); + + // Chain to authenticated_transfer with instruction=0 (init path) and pda_seeds + // to authorize the PDA. authenticated_transfer will claim it with Claim::Authorized. + let mut auth_pda_pre = pda_pre; + auth_pda_pre.is_authorized = true; + let auth_call = ChainedCall::new(auth_transfer_id, vec![auth_pda_pre], &0_u128) + .with_pda_seeds(vec![pda_seed]); + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + pre_states, + vec![pda_post], + ) + .with_chained_calls(vec![auth_call]) + .write(); + } +} diff --git a/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs new file mode 100644 index 00000000..c02261f9 --- /dev/null +++ b/test_program_methods/guest/src/bin/pda_fund_spend_proxy.rs @@ -0,0 +1,70 @@ +use nssa_core::{ + account::AccountWithMetadata, + program::{ + AccountPostState, ChainedCall, PdaSeed, ProgramId, ProgramInput, ProgramOutput, + read_nssa_inputs, + }, +}; +use risc0_zkvm::serde::to_vec; + +/// Proxy for interacting with private PDAs via `auth_transfer`. +/// +/// The `is_fund` flag selects the operating mode: +/// +/// - `false` (Spend): `pre_states = [pda (authorized), recipient]`. Debits the PDA. The PDA-to-npk +/// binding is established via `pda_seeds` in the chained call to `auth_transfer`. +/// +/// - `true` (Fund): `pre_states = [sender (authorized), pda (foreign/uninitialized)]`. Credits the +/// PDA. A direct call to `auth_transfer` cannot bind the PDA because `auth_transfer` uses +/// `Claim::Authorized`, not `Claim::Pda`. Routing through this proxy establishes the binding via +/// `pda_seeds` in the chained call. +type Instruction = (PdaSeed, u128, ProgramId, bool); + +fn main() { + let ( + ProgramInput { + self_program_id, + caller_program_id, + pre_states, + instruction: (seed, amount, auth_transfer_id, is_fund), + }, + instruction_words, + ) = read_nssa_inputs::(); + + let Ok([first, second]) = <[_; 2]>::try_from(pre_states) else { + return; + }; + + assert!(first.is_authorized, "first pre_state must be authorized"); + + let chained_pre_states = if is_fund { + let pda_authorized = AccountWithMetadata { + account: second.account.clone(), + account_id: second.account_id, + is_authorized: true, + }; + vec![first.clone(), pda_authorized] + } else { + vec![first.clone(), second.clone()] + }; + + let first_post = AccountPostState::new(first.account.clone()); + let second_post = AccountPostState::new(second.account.clone()); + + let chained_call = ChainedCall { + program_id: auth_transfer_id, + instruction_data: to_vec(&amount).unwrap(), + pre_states: chained_pre_states, + pda_seeds: vec![seed], + }; + + ProgramOutput::new( + self_program_id, + caller_program_id, + instruction_words, + vec![first, second], + vec![first_post, second_post], + ) + .with_chained_calls(vec![chained_call]) + .write(); +} diff --git a/testnet_initial_state/src/lib.rs b/testnet_initial_state/src/lib.rs index f6f1e288..b5c91d4d 100644 --- a/testnet_initial_state/src/lib.rs +++ b/testnet_initial_state/src/lib.rs @@ -103,7 +103,10 @@ pub struct PrivateAccountPrivateInitialData { impl PrivateAccountPrivateInitialData { #[must_use] pub fn account_id(&self) -> nssa::AccountId { - nssa::AccountId::from((&self.key_chain.nullifier_public_key, self.identifier)) + nssa::AccountId::for_regular_private_account( + &self.key_chain.nullifier_public_key, + self.identifier, + ) } } @@ -208,7 +211,7 @@ pub fn initial_state() -> V03State { .iter() .map(|init_comm_data| { let npk = &init_comm_data.npk; - let account_id = nssa::AccountId::from((npk, 0)); + let account_id = nssa::AccountId::for_regular_private_account(npk, 0); let mut acc = init_comm_data.account.clone(); diff --git a/wallet-ffi/Cargo.toml b/wallet-ffi/Cargo.toml index 0af20a54..37d552ec 100644 --- a/wallet-ffi/Cargo.toml +++ b/wallet-ffi/Cargo.toml @@ -15,6 +15,7 @@ wallet.workspace = true nssa.workspace = true nssa_core.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } + tokio.workspace = true [build-dependencies] diff --git a/wallet/src/chain_storage.rs b/wallet/src/chain_storage.rs index 3bfdb383..1b883e2f 100644 --- a/wallet/src/chain_storage.rs +++ b/wallet/src/chain_storage.rs @@ -11,6 +11,7 @@ use key_protocol::{ }; use log::debug; use nssa::program::Program; +use nssa_core::PrivateAccountKind; use crate::config::{InitialAccountData, Label, PersistentAccountData, WalletConfig}; @@ -78,8 +79,8 @@ impl WalletChainStore { PersistentAccountData::Private(data) => { let npk = data.data.value.0.nullifier_public_key; let chain_index = data.chain_index; - for identifier in &data.identifiers { - let account_id = nssa::AccountId::from((&npk, *identifier)); + for kind in &data.kinds { + let account_id = nssa::AccountId::for_private_account(&npk, kind); private_tree .account_id_map .insert(account_id, chain_index.clone()); @@ -95,7 +96,10 @@ impl WalletChainStore { data.account_id(), UserPrivateAccountData { key_chain: data.key_chain, - accounts: vec![(data.identifier, data.account)], + accounts: vec![( + PrivateAccountKind::Regular(data.identifier), + data.account, + )], }, ); } @@ -141,7 +145,7 @@ impl WalletChainStore { account_id, UserPrivateAccountData { key_chain: data.key_chain, - accounts: vec![(data.identifier, account)], + accounts: vec![(PrivateAccountKind::Regular(data.identifier), account)], }, ); } @@ -196,7 +200,7 @@ impl WalletChainStore { pub fn insert_private_account_data( &mut self, account_id: nssa::AccountId, - identifier: nssa_core::Identifier, + kind: &PrivateAccountKind, account: nssa_core::account::Account, ) { debug!("inserting at address {account_id}, this account {account:?}"); @@ -208,10 +212,10 @@ impl WalletChainStore { .entry(account_id) { let entry = entry.get_mut(); - if let Some((_, acc)) = entry.accounts.iter_mut().find(|(id, _)| *id == identifier) { + if let Some((_, acc)) = entry.accounts.iter_mut().find(|(k, _)| k == kind) { *acc = account; } else { - entry.accounts.push((identifier, account)); + entry.accounts.push((kind.clone(), account)); } return; } @@ -234,24 +238,21 @@ impl WalletChainStore { .key_map .get_mut(&chain_index) { - if let Some((_, acc)) = node.value.1.iter_mut().find(|(id, _)| *id == identifier) { + if let Some((_, acc)) = node.value.1.iter_mut().find(|(k, _)| k == kind) { *acc = account; } else { - node.value.1.push((identifier, account)); + node.value.1.push((kind.clone(), account)); } } } else { // Node not yet in account_id_map — find it by checking all nodes for (ci, node) in &mut self.user_data.private_key_tree.key_map { - let expected_id = - nssa::AccountId::from((&node.value.0.nullifier_public_key, identifier)); - if expected_id == account_id { - if let Some((_, acc)) = - node.value.1.iter_mut().find(|(id, _)| *id == identifier) - { + let npk = &node.value.0.nullifier_public_key; + if nssa::AccountId::for_private_account(npk, kind) == account_id { + if let Some((_, acc)) = node.value.1.iter_mut().find(|(k, _)| k == kind) { *acc = account; } else { - node.value.1.push((identifier, account)); + node.value.1.push((kind.clone(), account)); } // Register in account_id_map self.user_data @@ -297,7 +298,7 @@ mod tests { data: Some(public_data), }), PersistentAccountData::Private(Box::new(PersistentAccountDataPrivate { - identifiers: vec![], + kinds: vec![], chain_index: ChainIndex::root(), data: private_data, })), diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5ed99d90..e2ae12e7 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -89,6 +89,27 @@ pub enum NewSubcommand { /// Label to assign to the new account. label: Option, }, + /// Create a shared private account from a group's GMS. + PrivateGms { + /// Group name to derive keys from. + group: String, + #[arg(short, long)] + /// Label to assign to the new account. + label: Option, + #[arg(long)] + /// Create a PDA account (requires --seed and --program-id). + pda: bool, + #[arg(long, requires = "pda")] + /// PDA seed as 64-character hex string. + seed: Option, + #[arg(long, requires = "pda")] + /// Program ID as hex string. + program_id: Option, + #[arg(long, requires = "pda")] + /// Identifier that diversifies this PDA within the (`program_id`, seed, npk) family. + /// Defaults to a random value if not specified. + identifier: Option, + }, /// Recommended for receiving from multiple senders: creates a key node (npk + vpk) without /// registering any account. PrivateAccountsKey { @@ -180,9 +201,73 @@ impl WalletSubcommand for NewSubcommand { ); wallet_core.store_persistent_data().await?; - Ok(SubcommandReturnValue::RegisterAccount { account_id }) } + Self::PrivateGms { + group, + label, + pda, + seed, + program_id, + identifier, + } => { + if let Some(label) = &label + && wallet_core + .storage + .labels + .values() + .any(|l| l.to_string() == *label) + { + anyhow::bail!("Label '{label}' is already in use by another account"); + } + + let info = if pda { + let seed_hex = seed.context("--seed is required for PDA accounts")?; + let pid_hex = + program_id.context("--program-id is required for PDA accounts")?; + + let seed_bytes: [u8; 32] = hex::decode(&seed_hex) + .context("Invalid seed hex")? + .try_into() + .map_err(|_err| anyhow::anyhow!("Seed must be exactly 32 bytes"))?; + let pda_seed = nssa_core::program::PdaSeed::new(seed_bytes); + + let pid_bytes = hex::decode(&pid_hex).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()); + } + + wallet_core.create_shared_pda_account( + &group, + pda_seed, + pid, + identifier.unwrap_or_else(rand::random), + )? + } else { + wallet_core.create_shared_regular_account(&group)? + }; + + if let Some(label) = label { + wallet_core + .storage + .labels + .insert(info.account_id.to_string(), Label::new(label)); + } + + println!("Shared account from group '{group}'"); + println!("AccountId: Private/{}", info.account_id); + println!("NPK: {}", hex::encode(info.npk.0)); + println!("VPK: {}", hex::encode(&info.vpk.0)); + + wallet_core.store_persistent_data().await?; + Ok(SubcommandReturnValue::RegisterAccount { + account_id: info.account_id, + }) + } Self::PrivateAccountsKey { cci } => { let chain_index = wallet_core.create_private_accounts_key(cci); diff --git a/wallet/src/cli/group.rs b/wallet/src/cli/group.rs new file mode 100644 index 00000000..e1dd9159 --- /dev/null +++ b/wallet/src/cli/group.rs @@ -0,0 +1,158 @@ +use anyhow::{Context as _, Result}; +use clap::Subcommand; +use key_protocol::key_management::group_key_holder::{GroupKeyHolder, SealingPublicKey}; + +use crate::{ + WalletCore, + cli::{SubcommandReturnValue, WalletSubcommand}, +}; + +/// Group key 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, + }, + /// List all groups. + #[command(visible_alias = "ls")] + List, + /// 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 sealing public key as hex string. + #[arg(long)] + key: String, + }, + /// Unseal a received GMS and store it (join a group). + /// Uses the wallet's dedicated sealing key (generated via `new-sealing-key`). + Join { + /// Human-readable name to store the group under. + name: String, + /// Sealed GMS as hex string (from the inviter). + #[arg(long)] + sealed: String, + }, + /// Generate a dedicated sealing key pair for GMS distribution. + /// Share the printed public key with group members so they can seal GMS for you. + NewSealingKey, +} + +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 + .group_key_holder(&name) + .is_some() + { + anyhow::bail!("Group '{name}' already exists"); + } + + let holder = GroupKeyHolder::new(); + wallet_core.insert_group_key_holder(name.clone(), holder); + wallet_core.store_persistent_data().await?; + + println!("Created group '{name}'"); + 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 in holders.keys() { + println!("{name}"); + } + } + Ok(SubcommandReturnValue::Empty) + } + + Self::Remove { name } => { + if wallet_core.remove_group_key_holder(&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, key } => { + let holder = wallet_core + .storage() + .user_data + .group_key_holder(&name) + .context(format!("Group '{name}' not found"))?; + + let key_bytes = hex::decode(&key).context("Invalid key hex")?; + let recipient_key = + key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( + key_bytes, + ); + + let sealed = holder.seal_for(&recipient_key); + println!("{}", hex::encode(&sealed)); + Ok(SubcommandReturnValue::Empty) + } + + Self::Join { name, sealed } => { + if wallet_core + .storage() + .user_data + .group_key_holder(&name) + .is_some() + { + anyhow::bail!("Group '{name}' already exists"); + } + + let sealing_key = + wallet_core.storage().user_data.sealing_secret_key.context( + "No sealing key found. Run 'wallet group new-sealing-key' first.", + )?; + + let sealed_bytes = hex::decode(&sealed).context("Invalid sealed hex")?; + + let holder = GroupKeyHolder::unseal(&sealed_bytes, &sealing_key) + .map_err(|e| anyhow::anyhow!("Failed to unseal: {e:?}"))?; + + wallet_core.insert_group_key_holder(name.clone(), holder); + wallet_core.store_persistent_data().await?; + + println!("Joined group '{name}'"); + Ok(SubcommandReturnValue::Empty) + } + + Self::NewSealingKey => { + if wallet_core.storage().user_data.sealing_secret_key.is_some() { + anyhow::bail!("Sealing key already exists. Each wallet has one sealing key."); + } + + let mut secret: nssa_core::encryption::Scalar = [0_u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut secret); + let public_key = SealingPublicKey::from_scalar(secret); + + wallet_core.set_sealing_secret_key(secret); + wallet_core.store_persistent_data().await?; + + println!("Sealing key generated."); + println!("Public key: {}", hex::encode(public_key.to_bytes())); + println!("Share this public key with group members so they can seal GMS for you."); + Ok(SubcommandReturnValue::Empty) + } + } + } +} diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 17a90f79..df0ac840 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -16,6 +16,7 @@ use crate::{ chain::ChainSubcommand, config::ConfigSubcommand, keycard::KeycardSubcommand, + group::GroupSubcommand, programs::{ amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, @@ -28,6 +29,7 @@ pub mod account; pub mod chain; pub mod config; pub mod keycard; +pub mod group; pub mod programs; pub(crate) trait WalletSubcommand { @@ -60,6 +62,9 @@ pub enum Command { /// Associated Token Account program interaction subcommand. #[command(subcommand)] Ata(AtaSubcommand), + /// Group key management (create, invite, join, derive keys). + #[command(subcommand)] + Group(GroupSubcommand), /// Check the wallet can connect to the node and builtin local programs /// match the remote versions. CheckHealth, @@ -172,6 +177,7 @@ pub async fn execute_subcommand( Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?, Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?, Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?, + Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?, Command::Config(config_subcommand) => { config_subcommand.handle_subcommand(wallet_core).await? } diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 185648e4..1503c48e 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -28,7 +28,7 @@ pub struct PersistentAccountDataPublic { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersistentAccountDataPrivate { - pub identifiers: Vec, + pub kinds: Vec, pub chain_index: ChainIndex, pub data: ChildKeysPrivate, } @@ -98,6 +98,21 @@ pub struct PersistentStorage { /// "2rnKprXqWGWJTkDZKsQbFXa4ctKRbapsdoTKQFnaVGG8"). #[serde(default)] pub labels: HashMap, + /// Group key holders for shared account management. + #[serde(default)] + pub group_key_holders: std::collections::BTreeMap< + String, + key_protocol::key_management::group_key_holder::GroupKeyHolder, + >, + /// Cached state of shared private accounts (PDA and regular). + #[serde(default)] + pub shared_private_accounts: std::collections::BTreeMap< + nssa::AccountId, + key_protocol::key_protocol_core::SharedAccountEntry, + >, + /// Dedicated sealing secret key for GMS distribution. + #[serde(default)] + pub sealing_secret_key: Option, } impl PersistentStorage { diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index 2222ffe6..25116abd 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -200,10 +200,10 @@ pub fn produce_data_for_storage( } for (chain_index, node) in &user_data.private_key_tree.key_map { - let identifiers = node.value.1.iter().map(|(id, _)| *id).collect(); + let kinds = node.value.1.iter().map(|(kind, _)| kind.clone()).collect(); vec_for_storage.push( PersistentAccountDataPrivate { - identifiers, + kinds, chain_index: chain_index.clone(), data: node.clone(), } @@ -222,12 +222,12 @@ pub fn produce_data_for_storage( } for entry in user_data.default_user_private_accounts.values() { - for (identifier, account) in &entry.accounts { + for (kind, account) in &entry.accounts { vec_for_storage.push( InitialAccountData::Private(Box::new(PrivateAccountPrivateInitialData { account: account.clone(), key_chain: entry.key_chain.clone(), - identifier: *identifier, + identifier: kind.identifier(), })) .into(), ); @@ -238,6 +238,9 @@ pub fn produce_data_for_storage( accounts: vec_for_storage, last_synced_block, labels, + group_key_holders: user_data.group_key_holders.clone(), + shared_private_accounts: user_data.shared_private_accounts.clone(), + sealing_secret_key: user_data.sealing_secret_key, } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 0de34efe..beeaf611 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -25,7 +25,8 @@ use nssa::{ }, }; use nssa_core::{ - Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData, + Commitment, MembershipProof, PrivateAccountKind, SharedSecretKey, account::Nonce, + program::InstructionData, }; pub use privacy_preserving_tx::PrivacyPreservingAccount; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; @@ -52,6 +53,13 @@ pub enum AccDecodeData { Decode(nssa_core::SharedSecretKey, AccountId), } +/// Info returned when creating a shared account. +pub struct SharedAccountInfo { + pub account_id: AccountId, + pub npk: nssa_core::NullifierPublicKey, + pub vpk: nssa_core::encryption::ViewingPublicKey, +} + #[derive(Debug, thiserror::Error)] pub enum ExecutionFailureKind { #[error("Failed to get data from sequencer")] @@ -101,6 +109,9 @@ impl WalletCore { accounts: persistent_accounts, last_synced_block, labels, + group_key_holders, + shared_private_accounts, + sealing_secret_key, } = PersistentStorage::from_path(&storage_path).with_context(|| { format!( "Failed to read persistent storage at {}", @@ -112,7 +123,13 @@ impl WalletCore { config_path, storage_path, config_overrides, - |config| WalletChainStore::new(config, persistent_accounts, labels), + |config| { + let mut store = WalletChainStore::new(config, persistent_accounts, labels)?; + store.user_data.group_key_holders = group_key_holders; + store.user_data.shared_private_accounts = shared_private_accounts; + store.user_data.sealing_secret_key = sealing_secret_key; + Ok(store) + }, last_synced_block, ) } @@ -284,12 +301,178 @@ impl WalletCore { .value .0 .nullifier_public_key; - let account_id = AccountId::from((&npk, identifier)); - self.storage - .insert_private_account_data(account_id, identifier, Account::default()); + let account_id = AccountId::for_regular_private_account(&npk, identifier); + self.storage.insert_private_account_data( + account_id, + &PrivateAccountKind::Regular(identifier), + Account::default(), + ); (account_id, cci) } + /// Insert a group key holder into storage. + pub fn insert_group_key_holder( + &mut self, + name: String, + holder: key_protocol::key_management::group_key_holder::GroupKeyHolder, + ) { + self.storage.user_data.insert_group_key_holder(name, holder); + } + + /// Set the wallet's dedicated sealing secret key. + pub const fn set_sealing_secret_key(&mut self, key: nssa_core::encryption::Scalar) { + self.storage.user_data.sealing_secret_key = Some(key); + } + + /// Resolve an `AccountId` to the appropriate `PrivacyPreservingAccount` variant. + /// Checks the key tree first, then shared private accounts. + #[must_use] + pub fn resolve_private_account( + &self, + account_id: nssa::AccountId, + ) -> Option { + // Check key tree first + if self + .storage + .user_data + .get_private_account(account_id) + .is_some() + { + return Some(PrivacyPreservingAccount::PrivateOwned(account_id)); + } + + // Check shared private accounts + let entry = self.storage.user_data.shared_private_account(&account_id)?; + let holder = self + .storage + .user_data + .group_key_holder(&entry.group_label)?; + + if let (Some(pda_seed), Some(program_id)) = (entry.pda_seed, entry.pda_program_id) { + let keys = holder.derive_keys_for_pda(&program_id, &pda_seed); + Some(PrivacyPreservingAccount::PrivatePdaShared { + account_id, + nsk: keys.nullifier_secret_key, + npk: keys.generate_nullifier_public_key(), + vpk: keys.generate_viewing_public_key(), + identifier: entry.identifier, + }) + } else { + let derivation_seed = { + use sha2::Digest as _; + let mut hasher = sha2::Sha256::new(); + hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00"); + hasher.update(entry.identifier.to_le_bytes()); + let result: [u8; 32] = hasher.finalize().into(); + result + }; + let keys = holder.derive_keys_for_shared_account(&derivation_seed); + Some(PrivacyPreservingAccount::PrivateShared { + nsk: keys.nullifier_secret_key, + npk: keys.generate_nullifier_public_key(), + vpk: keys.generate_viewing_public_key(), + identifier: entry.identifier, + }) + } + } + + /// Remove a group key holder from storage. Returns the removed holder if it existed. + pub fn remove_group_key_holder( + &mut self, + name: &str, + ) -> Option { + self.storage.user_data.group_key_holders.remove(name) + } + + /// Register a shared account in storage for sync tracking. + fn register_shared_account( + &mut self, + account_id: AccountId, + group_label: String, + identifier: nssa_core::Identifier, + pda_seed: Option, + pda_program_id: Option, + ) { + use key_protocol::key_protocol_core::SharedAccountEntry; + self.storage.user_data.insert_shared_private_account( + account_id, + SharedAccountEntry { + group_label, + identifier, + pda_seed, + pda_program_id, + account: Account::default(), + }, + ); + } + + /// Create a shared PDA account from a group's GMS. Returns the `AccountId` and derived keys. + pub fn create_shared_pda_account( + &mut self, + group_name: &str, + pda_seed: nssa_core::program::PdaSeed, + program_id: nssa_core::program::ProgramId, + identifier: nssa_core::Identifier, + ) -> Result { + let holder = self + .storage + .user_data + .group_key_holder(group_name) + .context(format!("Group '{group_name}' not found"))?; + + let keys = holder.derive_keys_for_pda(&program_id, &pda_seed); + let npk = keys.generate_nullifier_public_key(); + let vpk = keys.generate_viewing_public_key(); + let account_id = AccountId::for_private_pda(&program_id, &pda_seed, &npk, identifier); + + self.register_shared_account( + account_id, + String::from(group_name), + identifier, + Some(pda_seed), + Some(program_id), + ); + + Ok(SharedAccountInfo { + account_id, + npk, + vpk, + }) + } + + /// Create a shared regular private account from a group's GMS. Returns the `AccountId` and + /// derived keys. The derivation seed is computed deterministically from a random identifier. + pub fn create_shared_regular_account(&mut self, group_name: &str) -> Result { + let identifier: nssa_core::Identifier = rand::random(); + let derivation_seed = { + use sha2::Digest as _; + let mut hasher = sha2::Sha256::new(); + hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00"); + hasher.update(identifier.to_le_bytes()); + let result: [u8; 32] = hasher.finalize().into(); + result + }; + + let holder = self + .storage + .user_data + .group_key_holder(group_name) + .context(format!("Group '{group_name}' not found"))?; + + let keys = holder.derive_keys_for_shared_account(&derivation_seed); + let npk = keys.generate_nullifier_public_key(); + let vpk = keys.generate_viewing_public_key(); + let account_id = AccountId::from((&npk, identifier)); + + self.register_shared_account(account_id, String::from(group_name), identifier, None, None); + + Ok(SharedAccountInfo { + account_id, + npk, + vpk, + }) + } + /// Get account balance. pub async fn get_account_balance(&self, acc: AccountId) -> Result { Ok(self.sequencer_client.get_account_balance(acc).await?) @@ -325,8 +508,17 @@ impl WalletCore { #[must_use] pub fn get_private_account_commitment(&self, account_id: AccountId) -> Option { - let (_keys, account, _identifier) = - self.storage.user_data.get_private_account(account_id)?; + let account = self + .storage + .user_data + .get_private_account(account_id) + .map(|(_keys, account, _identifier)| account) + .or_else(|| { + self.storage + .user_data + .shared_private_account(&account_id) + .map(|entry| entry.account.clone()) + })?; Some(Commitment::new(&account_id, &account)) } @@ -360,7 +552,7 @@ impl WalletCore { let acc_ead = tx.message.encrypted_private_post_states[output_index].clone(); let acc_comm = tx.message.new_commitments[output_index].clone(); - let (identifier, res_acc) = nssa_core::EncryptionScheme::decrypt( + let (kind, res_acc) = nssa_core::EncryptionScheme::decrypt( &acc_ead.ciphertext, secret, &acc_comm, @@ -373,7 +565,7 @@ impl WalletCore { println!("Received new acc {res_acc:#?}"); self.storage - .insert_private_account_data(*acc_account_id, identifier, res_acc); + .insert_private_account_data(*acc_account_id, &kind, res_acc); } AccDecodeData::Skip => {} } @@ -416,13 +608,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(); @@ -542,24 +728,93 @@ impl WalletCore { .try_into() .expect("Ciphertext ID is expected to fit in u32"), ) - .map(|(identifier, res_acc)| { - let account_id = nssa::AccountId::from(( - &key_chain.nullifier_public_key, - identifier, - )); - (account_id, identifier, res_acc) + .map(|(kind, res_acc)| { + let npk = &key_chain.nullifier_public_key; + let account_id = nssa::AccountId::for_private_account(npk, &kind); + (account_id, kind, res_acc) }) }) .collect::>() }) .collect::>(); - for (affected_account_id, identifier, new_acc) in affected_accounts { + for (affected_account_id, kind, new_acc) in affected_accounts { info!( "Received new account for account_id {affected_account_id:#?} with account object {new_acc:#?}" ); self.storage - .insert_private_account_data(affected_account_id, identifier, new_acc); + .insert_private_account_data(affected_account_id, &kind, new_acc); + } + + // Scan for updates to shared accounts (GMS-derived). + self.sync_shared_private_accounts_with_tx(&tx); + } + + fn sync_shared_private_accounts_with_tx(&mut self, tx: &PrivacyPreservingTransaction) { + let shared_keys: Vec<_> = self + .storage + .user_data + .shared_private_accounts_iter() + .filter_map(|(&account_id, entry)| { + let holder = self + .storage + .user_data + .group_key_holder(&entry.group_label)?; + + let keys = match (&entry.pda_seed, &entry.pda_program_id) { + (Some(pda_seed), Some(program_id)) => { + holder.derive_keys_for_pda(program_id, pda_seed) + } + (Some(_), None) => return None, // PDA without program_id, skip + _ => { + let derivation_seed = { + use sha2::Digest as _; + let mut hasher = sha2::Sha256::new(); + hasher.update(b"/LEE/v0.3/SharedAccountTag/\x00\x00\x00\x00\x00"); + hasher.update(entry.identifier.to_le_bytes()); + let result: [u8; 32] = hasher.finalize().into(); + result + }; + holder.derive_keys_for_shared_account(&derivation_seed) + } + }; + let npk = keys.generate_nullifier_public_key(); + let vpk = keys.generate_viewing_public_key(); + let vsk = keys.viewing_secret_key; + Some((account_id, npk, vpk, vsk)) + }) + .collect(); + + for (account_id, npk, vpk, vsk) in shared_keys { + let view_tag = EncryptedAccountData::compute_view_tag(&npk, &vpk); + + for (ciph_id, encrypted_data) in tx + .message() + .encrypted_private_post_states + .iter() + .enumerate() + { + if encrypted_data.view_tag != view_tag { + continue; + } + + let shared_secret = SharedSecretKey::new(&vsk, &encrypted_data.epk); + let commitment = &tx.message.new_commitments[ciph_id]; + + if let Some((_kind, new_acc)) = nssa_core::EncryptionScheme::decrypt( + &encrypted_data.ciphertext, + &shared_secret, + commitment, + ciph_id + .try_into() + .expect("Ciphertext ID is expected to fit in u32"), + ) { + info!("Synced shared account {account_id:#?} with new state {new_acc:#?}"); + self.storage + .user_data + .update_shared_private_account_state(&account_id, new_acc); + } + } } } diff --git a/wallet/src/pinata_interactions.rs b/wallet/src/pinata_interactions.rs deleted file mode 100644 index 77549772..00000000 --- a/wallet/src/pinata_interactions.rs +++ /dev/null @@ -1,165 +0,0 @@ -use common::{HashType, transaction::NSSATransaction}; -use sequencer_service_rpc::RpcClient as _; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use nssa::{AccountId, privacy_preserving_transaction::circuit}; -use nssa_core::{MembershipProof, SharedSecretKey, account::AccountWithMetadata}; - -use crate::{ - ExecutionFailureKind, WalletCore, helperfunctions::produce_random_nonces, - transaction_utils::AccountPreparedData, -}; - -impl WalletCore { - pub async fn claim_pinata( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - ) -> Result { - let account_ids = vec![pinata_account_id, winner_account_id]; - let program_id = nssa::program::Program::pinata().id(); - let message = - nssa::public_transaction::Message::try_new(program_id, account_ids, vec![], solution) - .unwrap(); - - let witness_set = nssa::public_transaction::WitnessSet::for_message(&message, &[]); - let tx = nssa::PublicTransaction::new(message, witness_set); - - Ok(self.sequencer_client.send_transaction(NSSATransaction::Public(tx).into()).await?) - } - - pub async fn claim_pinata_private_owned_account_already_initialized( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - winner_proof: MembershipProof, - ) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: winner_nsk, - npk: winner_npk, - vpk: winner_vpk, - auth_acc: winner_pre, - proof: _, - } = self - .private_acc_preparation(winner_account_id, true, false) - .await?; - - let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); - - let program = nssa::program::Program::pinata(); - - let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); - - let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); - let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[pinata_pre, winner_pre], - &nssa::program::Program::serialize_instruction(solution).unwrap(), - &[0, 1], - &produce_random_nonces(1), - &[(winner_npk, shared_secret_winner.clone())], - &[(winner_nsk.unwrap())], - &[winner_proof], - &program.into(), - ) - .unwrap(); - - let message = - nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - vec![pinata_account_id], - vec![], - vec![( - winner_npk, - winner_vpk.clone(), - eph_holder_winner.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &[], - ); - let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( - message, - witness_set, - ); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_winner], - )) - } - - pub async fn claim_pinata_private_owned_account_not_initialized( - &self, - pinata_account_id: AccountId, - winner_account_id: AccountId, - solution: u128, - ) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: _, - npk: winner_npk, - vpk: winner_vpk, - auth_acc: winner_pre, - proof: _, - } = self - .private_acc_preparation(winner_account_id, false, false) - .await?; - - let pinata_acc = self.get_account_public(pinata_account_id).await.unwrap(); - - let program = nssa::program::Program::pinata(); - - let pinata_pre = AccountWithMetadata::new(pinata_acc.clone(), false, pinata_account_id); - - let eph_holder_winner = EphemeralKeyHolder::new(&winner_npk); - let shared_secret_winner = eph_holder_winner.calculate_shared_secret_sender(&winner_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[pinata_pre, winner_pre], - &nssa::program::Program::serialize_instruction(solution).unwrap(), - &[0, 2], - &produce_random_nonces(1), - &[(winner_npk, shared_secret_winner.clone())], - &[], - &[], - &program.into(), - ) - .unwrap(); - - let message = - nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output( - vec![pinata_account_id], - vec![], - vec![( - winner_npk, - winner_vpk.clone(), - eph_holder_winner.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &[], - ); - let tx = nssa::privacy_preserving_transaction::PrivacyPreservingTransaction::new( - message, - witness_set, - ); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_winner], - )) - } -} diff --git a/wallet/src/privacy_preserving_tx.rs b/wallet/src/privacy_preserving_tx.rs index 3df2ecc1..6d118c0c 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}, }; @@ -18,6 +19,35 @@ pub enum PrivacyPreservingAccount { vpk: ViewingPublicKey, identifier: Identifier, }, + /// An owned private PDA: wallet holds the nsk/npk; `account_id` was derived via + /// [`AccountId::for_private_pda`]. + PrivatePdaOwned(AccountId), + /// A foreign private PDA: wallet knows the recipient's npk/vpk but not their nsk. + /// Uses a default (uninitialised) account. + PrivatePdaForeign { + account_id: AccountId, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, + }, + /// A shared regular private account with externally-provided keys (e.g. from GMS). + /// Uses standard `AccountId = from((&npk, identifier))` with authorized/unauthorized private + /// paths. Works with `authenticated_transfer` and all existing programs out of the box. + PrivateShared { + nsk: NullifierSecretKey, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, + }, + /// A shared private PDA with externally-provided keys (e.g. from GMS). + /// `account_id` was derived via [`AccountId::for_private_pda`]. + PrivatePdaShared { + account_id: AccountId, + nsk: NullifierSecretKey, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, + }, } impl PrivacyPreservingAccount { @@ -31,18 +61,17 @@ impl PrivacyPreservingAccount { matches!( &self, Self::PrivateOwned(_) - | Self::PrivateForeign { - npk: _, - vpk: _, - identifier: _ - } + | Self::PrivateForeign { .. } + | Self::PrivatePdaOwned(_) + | Self::PrivatePdaForeign { .. } + | Self::PrivateShared { .. } + | Self::PrivatePdaShared { .. } ) } } pub struct PrivateAccountKeys { pub npk: NullifierPublicKey, - pub identifier: Identifier, pub ssk: SharedSecretKey, pub vpk: ViewingPublicKey, pub epk: EphemeralPublicKey, @@ -58,7 +87,6 @@ enum State { pub struct AccountManager { states: Vec, - visibility_mask: Vec, } impl AccountManager { @@ -66,11 +94,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) @@ -80,13 +107,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_key_tree_acc_preparation(wallet, account_id, false).await?; - (State::Private(pre), mask) + State::Private(pre) } PrivacyPreservingAccount::PrivateForeign { npk, @@ -95,6 +121,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, @@ -102,20 +131,75 @@ 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_key_tree_acc_preparation(wallet, account_id, true).await?; + State::Private(pre) + } + PrivacyPreservingAccount::PrivatePdaForeign { + account_id, + npk, + vpk, + identifier, + } => { + 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, + identifier, + vpk, + pre_state: auth_acc, + proof: None, + ssk, + epk, + is_pda: true, + }; + State::Private(pre) + } + PrivacyPreservingAccount::PrivateShared { + nsk, + npk, + vpk, + identifier, + } => { + let account_id = nssa::AccountId::from((&npk, identifier)); + let pre = private_shared_acc_preparation( + wallet, account_id, nsk, npk, vpk, identifier, false, + ) + .await?; + + State::Private(pre) + } + PrivacyPreservingAccount::PrivatePdaShared { + account_id, + nsk, + npk, + vpk, + identifier, + } => { + let pre = private_shared_acc_preparation( + wallet, account_id, nsk, npk, vpk, identifier, true, + ) + .await?; + + 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 { @@ -128,10 +212,6 @@ impl AccountManager { .collect() } - pub fn visibility_mask(&self) -> &[u8] { - &self.visibility_mask - } - pub fn public_account_nonces(&self) -> Vec { self.states .iter() @@ -146,38 +226,59 @@ 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, + /// 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, - ssk: eph_holder.calculate_shared_secret_sender(&pre.vpk), - vpk: pre.vpk.clone(), - epk: eph_holder.generate_ephemeral_public_key(), - }) - } - 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, + }, + _ => InputAccountIdentity::PrivatePdaInit { + npk: pre.npk, + ssk: pre.ssk, + identifier: pre.identifier, + }, + }, + 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() } @@ -210,20 +311,30 @@ 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( +async fn private_key_tree_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) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; + let (from_keys, from_acc, from_identifier) = wallet + .storage + .user_data + .get_private_account(account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; let nsk = from_keys.private_key_holder.nullifier_secret_key; - let from_npk = from_keys.nullifier_public_key; let from_vpk = from_keys.viewing_public_key; @@ -235,7 +346,11 @@ async fn private_acc_preparation( // TODO: Technically we could allow unauthorized owned accounts, but currently we don't have // support from that in the wallet. - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, (&from_npk, from_identifier)); + 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), @@ -244,5 +359,64 @@ async fn private_acc_preparation( vpk: from_vpk, pre_state: sender_pre, proof, + ssk, + epk, + is_pda, }) } + +async fn private_shared_acc_preparation( + wallet: &WalletCore, + account_id: AccountId, + nsk: NullifierSecretKey, + npk: NullifierPublicKey, + vpk: ViewingPublicKey, + identifier: Identifier, + is_pda: bool, +) -> Result { + let acc = wallet + .storage + .user_data + .shared_private_account(&account_id) + .map(|e| e.account.clone()) + .unwrap_or_default(); + + let pre_state = AccountWithMetadata::new(acc, true, account_id); + + let proof = wallet + .check_private_account_initialized(account_id) + .await + .unwrap_or(None); + + let eph_holder = EphemeralKeyHolder::new(&npk); + let ssk = eph_holder.calculate_shared_secret_sender(&vpk); + let epk = eph_holder.generate_ephemeral_public_key(); + Ok(AccountPreparedData { + nsk: Some(nsk), + npk, + identifier, + vpk, + pre_state, + proof, + ssk, + epk, + is_pda, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn private_shared_is_private() { + let acc = PrivacyPreservingAccount::PrivateShared { + nsk: [0; 32], + npk: NullifierPublicKey([1; 32]), + vpk: ViewingPublicKey::from_scalar([2; 32]), + identifier: 42, + }; + assert!(acc.is_private()); + assert!(!acc.is_public()); + } +} diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs index ac60fb63..fa868750 100644 --- a/wallet/src/program_facades/ata.rs +++ b/wallet/src/program_facades/ata.rs @@ -188,7 +188,9 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(definition_id), PrivacyPreservingAccount::Public(ata_id), ]; @@ -223,7 +225,9 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(sender_ata_id), PrivacyPreservingAccount::Public(recipient_id), ]; @@ -257,7 +261,9 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_ata_id), PrivacyPreservingAccount::Public(definition_id), ]; diff --git a/wallet/src/program_facades/native_token_transfer/deshielded.rs b/wallet/src/program_facades/native_token_transfer/deshielded.rs index d51f15ce..d4bde39f 100644 --- a/wallet/src/program_facades/native_token_transfer/deshielded.rs +++ b/wallet/src/program_facades/native_token_transfer/deshielded.rs @@ -16,7 +16,9 @@ impl NativeTokenTransfer<'_> { self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - PrivacyPreservingAccount::PrivateOwned(from), + self.0 + .resolve_private_account(from) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(to), ], instruction_data, diff --git a/wallet/src/program_facades/native_token_transfer/private.rs b/wallet/src/program_facades/native_token_transfer/private.rs index d317b31c..501ead50 100644 --- a/wallet/src/program_facades/native_token_transfer/private.rs +++ b/wallet/src/program_facades/native_token_transfer/private.rs @@ -14,9 +14,14 @@ impl NativeTokenTransfer<'_> { ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let instruction: u128 = 0; + let account = self + .0 + .resolve_private_account(from) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + self.0 .send_privacy_preserving_tx( - vec![PrivacyPreservingAccount::PrivateOwned(from)], + vec![account], Program::serialize_instruction(instruction).unwrap(), &Program::authenticated_transfer_program().into(), ) @@ -41,7 +46,9 @@ impl NativeTokenTransfer<'_> { self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - PrivacyPreservingAccount::PrivateOwned(from), + self.0 + .resolve_private_account(from) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::PrivateForeign { npk: to_npk, vpk: to_vpk, @@ -69,12 +76,18 @@ impl NativeTokenTransfer<'_> { ) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + let from_account = self + .0 + .resolve_private_account(from) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let to_account = self + .0 + .resolve_private_account(to) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + self.0 .send_privacy_preserving_tx_with_pre_check( - vec![ - PrivacyPreservingAccount::PrivateOwned(from), - PrivacyPreservingAccount::PrivateOwned(to), - ], + vec![from_account, to_account], instruction_data, &program.into(), tx_pre_check, diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 8f7ba2b5..98dd0081 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -18,7 +18,9 @@ impl NativeTokenTransfer<'_> { .send_privacy_preserving_tx_with_pre_check( vec![ PrivacyPreservingAccount::Public(from), - PrivacyPreservingAccount::PrivateOwned(to), + self.0 + .resolve_private_account(to) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &program.into(), diff --git a/wallet/src/program_facades/pinata.rs b/wallet/src/program_facades/pinata.rs index 97118ecd..0575455e 100644 --- a/wallet/src/program_facades/pinata.rs +++ b/wallet/src/program_facades/pinata.rs @@ -56,7 +56,9 @@ impl Pinata<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(pinata_account_id), - PrivacyPreservingAccount::PrivateOwned(winner_account_id), + self.0 + .resolve_private_account(winner_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], nssa::program::Program::serialize_instruction(solution).unwrap(), &nssa::program::Program::pinata().into(), diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index d105a4de..da069bc2 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -74,7 +74,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(supply_account_id), + self.0 + .resolve_private_account(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -103,7 +105,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(supply_account_id), ], instruction_data, @@ -133,8 +137,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(supply_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -227,8 +235,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), - PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(recipient_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -259,7 +271,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, @@ -293,7 +307,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(recipient_account_id), ], instruction_data, @@ -325,7 +341,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(sender_account_id), - PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + self.0 + .resolve_private_account(recipient_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -434,8 +452,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -464,7 +486,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_account_id), ], instruction_data, @@ -496,7 +520,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -590,8 +616,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -622,7 +652,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::PrivateForeign { npk: holder_npk, vpk: holder_vpk, @@ -656,7 +688,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_account_id), ], instruction_data, @@ -688,7 +722,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), diff --git a/wallet/src/transaction_utils.rs b/wallet/src/transaction_utils.rs deleted file mode 100644 index f2def802..00000000 --- a/wallet/src/transaction_utils.rs +++ /dev/null @@ -1,594 +0,0 @@ -use common::{HashType, transaction::NSSATransaction}; -use sequencer_service_rpc::RpcClient as _; -use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use nssa::{ - Account, AccountId, PrivacyPreservingTransaction, - privacy_preserving_transaction::{circuit, message::Message, witness_set::WitnessSet}, - program::Program, -}; -use nssa_core::{ - Commitment, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, - account::AccountWithMetadata, encryption::ViewingPublicKey, program::InstructionData, -}; - -use crate::{ExecutionFailureKind, WalletCore, helperfunctions::produce_random_nonces}; - -pub(crate) struct AccountPreparedData { - pub nsk: Option, - pub npk: NullifierPublicKey, - pub vpk: ViewingPublicKey, - pub auth_acc: AccountWithMetadata, - pub proof: Option, -} - -impl WalletCore { - pub(crate) async fn private_acc_preparation( - &self, - account_id: AccountId, - is_authorized: bool, - needs_proof: bool, - ) -> Result { - let Some((from_keys, from_acc)) = self - .storage - .user_data - .get_private_account(&account_id) - .cloned() - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let mut nsk = None; - let mut proof = None; - - let from_npk = from_keys.nullifier_public_key; - let from_vpk = from_keys.viewing_public_key; - - let sender_commitment = Commitment::new(&from_npk, &from_acc); - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), is_authorized, &from_npk); - - if is_authorized { - nsk = Some(from_keys.private_key_holder.nullifier_secret_key); - } - - if needs_proof { - proof = Some( - self.sequencer_client - .get_proof_for_commitment(sender_commitment) - .await - .unwrap(), - ); - } - - Ok(AccountPreparedData { - nsk, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof, - }) - } - - pub(crate) async fn private_tx_two_accs_all_init( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - to_proof: MembershipProof, - ) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let AccountPreparedData { - nsk: to_nsk, - npk: to_npk, - vpk: to_vpk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, true, false).await?; - - tx_pre_check(&sender_pre.account, &recipient_pre.account)?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk); - - let eph_holder_to = EphemeralKeyHolder::new(&to_npk); - let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 1], - &produce_random_nonces(2), - &[ - (from_npk, shared_secret_from.clone()), - (to_npk, shared_secret_to.clone()), - ], - &[ - (from_nsk.unwrap(), from_proof.unwrap()), - (to_nsk.unwrap(), to_proof), - ], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk, - from_vpk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - ), - ( - to_npk, - to_vpk.clone(), - eph_holder_to.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn private_tx_two_accs_receiver_uninit( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let AccountPreparedData { - nsk: _, - npk: to_npk, - vpk: to_vpk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, false, false).await?; - - tx_pre_check(&sender_pre.account, &recipient_pre.account)?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk); - - let eph_holder_to = EphemeralKeyHolder::new(&to_npk); - let shared_secret_to = eph_holder_to.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 2], - &produce_random_nonces(2), - &[ - (from_npk, shared_secret_from.clone()), - (to_npk, shared_secret_to.clone()), - ], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk, - from_vpk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - ), - ( - to_npk, - to_vpk.clone(), - eph_holder_to.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn private_tx_two_accs_receiver_outer( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_vpk: ViewingPublicKey, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(HashType, [SharedSecretKey; 2]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let to_acc = nssa_core::account::Account::default(); - - tx_pre_check(&sender_pre.account, &to_acc)?; - - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - - let shared_secret_from = eph_holder.calculate_shared_secret_sender(&from_vpk); - let shared_secret_to = eph_holder.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 2], - &produce_random_nonces(2), - &[ - (from_npk, shared_secret_from.clone()), - (to_npk, shared_secret_to.clone()), - ], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - ( - from_npk, - from_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - ), - ( - to_npk, - to_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - ), - ], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_from, shared_secret_to], - )) - } - - pub(crate) async fn deshielded_tx_two_accs( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(HashType, [nssa_core::SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: from_nsk, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof: from_proof, - } = self.private_acc_preparation(from, true, true).await?; - - let Ok(to_acc) = self.get_account_public(to).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - tx_pre_check(&sender_pre.account, &to_acc)?; - - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, to); - - let eph_holder = EphemeralKeyHolder::new(&from_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&from_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[1, 0], - &produce_random_nonces(1), - &[(from_npk, shared_secret.clone())], - &[(from_nsk.unwrap(), from_proof.unwrap())], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![to], - vec![], - vec![( - from_npk, - from_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_all_init( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - to_proof: MembershipProof, - ) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let AccountPreparedData { - nsk: to_nsk, - npk: to_npk, - vpk: to_vpk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, true, false).await?; - - tx_pre_check(&from_acc, &recipient_pre.account)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 1], - &produce_random_nonces(1), - &[(to_npk, shared_secret.clone())], - &[(to_nsk.unwrap(), to_proof)], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk, - to_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_receiver_uninit( - &self, - from: AccountId, - to: AccountId, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let AccountPreparedData { - nsk: _, - npk: to_npk, - vpk: to_vpk, - auth_acc: recipient_pre, - proof: _, - } = self.private_acc_preparation(to, false, false).await?; - - tx_pre_check(&from_acc, &recipient_pre.account)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 2], - &produce_random_nonces(1), - &[(to_npk, shared_secret.clone())], - &[], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk, - to_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret], - )) - } - - pub(crate) async fn shielded_two_accs_receiver_outer( - &self, - from: AccountId, - to_npk: NullifierPublicKey, - to_vpk: ViewingPublicKey, - instruction_data: InstructionData, - tx_pre_check: impl FnOnce(&Account, &Account) -> Result<(), ExecutionFailureKind>, - program: Program, - ) -> Result { - let Ok(from_acc) = self.get_account_public(from).await else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let to_acc = Account::default(); - - tx_pre_check(&from_acc, &to_acc)?; - - let sender_pre = AccountWithMetadata::new(from_acc.clone(), true, from); - let recipient_pre = AccountWithMetadata::new(to_acc.clone(), false, &to_npk); - - let eph_holder = EphemeralKeyHolder::new(&to_npk); - let shared_secret = eph_holder.calculate_shared_secret_sender(&to_vpk); - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre, recipient_pre], - &instruction_data, - &[0, 2], - &produce_random_nonces(1), - &[(to_npk, shared_secret.clone())], - &[], - &program.into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![from], - vec![from_acc.nonce], - vec![( - to_npk, - to_vpk.clone(), - eph_holder.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); - - let Some(signing_key) = signing_key else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - - let witness_set = WitnessSet::for_message(&message, proof, &[signing_key]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?) - } - - pub async fn register_account_under_authenticated_transfers_programs_private( - &self, - from: AccountId, - ) -> Result<(HashType, [SharedSecretKey; 1]), ExecutionFailureKind> { - let AccountPreparedData { - nsk: _, - npk: from_npk, - vpk: from_vpk, - auth_acc: sender_pre, - proof: _, - } = self.private_acc_preparation(from, false, false).await?; - - let eph_holder_from = EphemeralKeyHolder::new(&from_npk); - let shared_secret_from = eph_holder_from.calculate_shared_secret_sender(&from_vpk); - - let instruction: u128 = 0; - - let (output, proof) = circuit::execute_and_prove( - &[sender_pre], - &Program::serialize_instruction(instruction).unwrap(), - &[2], - &produce_random_nonces(1), - &[(from_npk, shared_secret_from.clone())], - &[], - &Program::authenticated_transfer_program().into(), - ) - .unwrap(); - - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![( - from_npk, - from_vpk.clone(), - eph_holder_from.generate_ephemeral_public_key(), - )], - output, - ) - .unwrap(); - - let witness_set = WitnessSet::for_message(&message, proof, &[]); - let tx = PrivacyPreservingTransaction::new(message, witness_set); - - Ok(( - self.sequencer_client.send_transaction(NSSATransaction::PrivacyPreserving(tx).into()).await?, - [shared_secret_from], - )) - } -}