diff --git a/.deny.toml b/.deny.toml index 8de5ab88..94b820f5 100644 --- a/.deny.toml +++ b/.deny.toml @@ -13,9 +13,11 @@ ignore = [ { id = "RUSTSEC-2025-0055", reason = "`tracing-subscriber` v0.2.25 pulled in by ark-relations v0.4.0 - will be addressed before mainnet" }, { 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-0118", reason = "`hickory-proto` v0.25.0-alpha.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" }, + { id = "RUSTSEC-2024-0370", reason = "transitive dependency of `logos-blockchain-http-api-common`, can't do anything than wait for upstream fix" }, + { id = "RUSTSEC-2026-0173", reason = "`proc-macro-error2` is unmaintained; pulled in transitively via `leptos_macro` and `overwatch-derive`, waiting on upstream fix" }, ] yanked = "deny" unused-ignored-advisory = "deny" @@ -56,6 +58,7 @@ unused-allowed-license = "deny" allow-git = [ "https://github.com/EspressoSystems/jellyfish.git", "https://github.com/logos-blockchain/logos-blockchain.git", + "https://github.com/logos-blockchain/logos-blockchain-circuits.git", ] unknown-git = "deny" unknown-registry = "deny" diff --git a/.gitignore b/.gitignore index b265e9aa..4605856c 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ result wallet-ffi/wallet_ffi.h bedrock_signing_key integration_tests/configs/debug/ +venv/ +keycard_wallet/python/__pycache__/ +keycard_wallet/python/keycard-py/ diff --git a/Cargo.lock b/Cargo.lock index ca35ce96..990b29b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,7 +273,7 @@ dependencies = [ "ark-std 0.4.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "sha2", ] @@ -293,7 +293,7 @@ dependencies = [ "ark-std 0.5.0", "blake2", "derivative", - "digest", + "digest 0.10.7", "fnv", "merlin", "sha2", @@ -359,7 +359,7 @@ dependencies = [ "ark-serialize 0.4.2", "ark-std 0.4.0", "derivative", - "digest", + "digest 0.10.7", "itertools 0.10.5", "num-bigint 0.4.6", "num-traits", @@ -379,7 +379,7 @@ dependencies = [ "ark-serialize 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "educe", "itertools 0.13.0", "num-bigint 0.4.6", @@ -541,7 +541,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ "ark-serialize-derive 0.4.2", "ark-std 0.4.0", - "digest", + "digest 0.10.7", "num-bigint 0.4.6", ] @@ -554,7 +554,7 @@ dependencies = [ "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", - "digest", + "digest 0.10.7", "num-bigint 0.4.6", ] @@ -1118,7 +1118,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1126,7 +1126,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -1164,9 +1164,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "bitvec" @@ -1186,7 +1186,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1221,7 +1221,7 @@ checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ "async-stream", "base64", - "bitflags 2.11.1", + "bitflags 2.12.1", "bollard-buildkit-proto", "bollard-stubs", "bytes", @@ -1447,14 +1447,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -1617,6 +1617,12 @@ dependencies = [ "lee_core", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -1751,6 +1757,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-str" version = "0.4.3" @@ -2018,7 +2030,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ + "getrandom 0.4.2", "hybrid-array", + "rand_core 0.10.1", ] [[package]] @@ -2040,6 +2054,15 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2049,7 +2072,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "serde", @@ -2180,7 +2203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2189,11 +2212,21 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -2312,11 +2345,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", + "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", +] + [[package]] name = "directories" version = "6.0.0" @@ -2410,7 +2453,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac1e888d6830712d565b2f3a974be3200be9296bc1b03db8251a4cbf18a4a34" dependencies = [ - "digest", + "digest 0.10.7", "futures", "rand 0.8.6", "reqwest", @@ -2442,13 +2485,13 @@ version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", - "digest", + "der 0.7.10", + "digest 0.10.7", "elliptic-curve", "rfc6979", "serdect", "signature", - "spki", + "spki 0.7.3", ] [[package]] @@ -2457,7 +2500,7 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "serde", "signature", ] @@ -2519,12 +2562,12 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array 0.14.7", "group", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "sec1", "serdect", @@ -2762,6 +2805,17 @@ dependencies = [ "serde", ] +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "ferroid" version = "2.0.0" @@ -3037,7 +3091,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "log", "managed", @@ -3068,9 +3122,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +checksum = "c2e55f16dcf0e9c00efbe2e655ffe45fc98e7066b52bc92f8a79e64060a79351" dependencies = [ "rustversion", "serde_core", @@ -3429,7 +3483,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -3551,6 +3605,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ + "ctutils", "typenum", ] @@ -3572,9 +3627,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -4036,15 +4091,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -4226,9 +4272,9 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", @@ -4239,9 +4285,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -4510,6 +4556,26 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kem" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" +dependencies = [ + "crypto-common 0.2.2", + "rand_core 0.10.1", +] + [[package]] name = "key_protocol" version = "0.1.0" @@ -4526,6 +4592,7 @@ dependencies = [ "k256", "lee", "lee_core", + "ml-kem", "rand 0.8.6", "serde", "sha2", @@ -4541,6 +4608,7 @@ dependencies = [ "pyo3", "serde", "serde_json", + "zeroize", ] [[package]] @@ -4634,7 +4702,7 @@ dependencies = [ "bytemuck", "bytesize", "chacha20", - "k256", + "ml-kem", "risc0-zkvm", "serde", "serde_json", @@ -5335,9 +5403,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -5379,14 +5447,14 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "logos-blockchain-blend-crypto" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "logos-blockchain-groth16", @@ -5400,7 +5468,7 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-message" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "blake2", "derivative", @@ -5424,10 +5492,10 @@ dependencies = [ [[package]] name = "logos-blockchain-blend-proofs" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-blend-crypto", "logos-blockchain-groth16", @@ -5444,7 +5512,7 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-broadcast-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "derivative", @@ -5460,7 +5528,7 @@ dependencies = [ [[package]] name = "logos-blockchain-chain-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -5489,19 +5557,88 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "logos-blockchain-circuits-build" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "dirs", + "fd-lock", + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "logos-blockchain-circuits-common" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poc-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-pol-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-poq-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + [[package]] name = "logos-blockchain-circuits-prover" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-utils", "tempfile", ] +[[package]] +name = "logos-blockchain-circuits-signature-sys" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "logos-blockchain-circuits-build", + "logos-blockchain-circuits-common", + "logos-blockchain-circuits-types", +] + +[[package]] +name = "logos-blockchain-circuits-types" +version = "0.5.0" +source = "git+https://github.com/logos-blockchain/logos-blockchain-circuits.git?rev=2e79ac30831d89e6a349720c08d5b8b9978970e0#2e79ac30831d89e6a349720c08d5b8b9978970e0" +dependencies = [ + "bytes", + "libc", +] + [[package]] name = "logos-blockchain-circuits-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "dirs", ] @@ -5509,7 +5646,7 @@ dependencies = [ [[package]] name = "logos-blockchain-common-http-client" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "futures", "hex", @@ -5531,7 +5668,7 @@ dependencies = [ [[package]] name = "logos-blockchain-core" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "bincode", @@ -5564,7 +5701,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-engine" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-pol", "logos-blockchain-utils", @@ -5579,7 +5716,7 @@ dependencies = [ [[package]] name = "logos-blockchain-cryptarchia-sync" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "bytes", "futures", @@ -5598,14 +5735,14 @@ dependencies = [ [[package]] name = "logos-blockchain-groth16" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-groth16 0.4.0", "ark-serialize 0.4.2", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "num-bigint 0.4.6", "serde", @@ -5616,7 +5753,7 @@ dependencies = [ [[package]] name = "logos-blockchain-http-api-common" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "axum 0.7.9", "logos-blockchain-core", @@ -5636,12 +5773,12 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-keys" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", "ed25519-dalek", - "generic-array 1.4.1", + "generic-array 1.4.3", "hex", "logos-blockchain-groth16", "logos-blockchain-key-management-system-macros", @@ -5662,7 +5799,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-macros" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "proc-macro2", "quote", @@ -5672,7 +5809,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-operators" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "logos-blockchain-blend-proofs", @@ -5688,7 +5825,7 @@ dependencies = [ [[package]] name = "logos-blockchain-key-management-system-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "log", @@ -5705,7 +5842,7 @@ dependencies = [ [[package]] name = "logos-blockchain-ledger" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "derivative", "logos-blockchain-blend-crypto", @@ -5731,7 +5868,7 @@ dependencies = [ [[package]] name = "logos-blockchain-libp2p" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "backon", @@ -5760,7 +5897,7 @@ dependencies = [ [[package]] name = "logos-blockchain-log-targets" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-log-targets-macros", ] @@ -5768,7 +5905,7 @@ dependencies = [ [[package]] name = "logos-blockchain-log-targets-macros" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "proc-macro2", "quote", @@ -5778,7 +5915,7 @@ dependencies = [ [[package]] name = "logos-blockchain-mmr" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -5791,7 +5928,7 @@ dependencies = [ [[package]] name = "logos-blockchain-network-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -5812,13 +5949,14 @@ dependencies = [ [[package]] name = "logos-blockchain-poc" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poc-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-proofs-error", - "logos-blockchain-witness-generator", "num-bigint 0.4.6", "serde", "serde_json", @@ -5828,15 +5966,16 @@ dependencies = [ [[package]] name = "logos-blockchain-pol" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "astro-float", + "logos-blockchain-circuits-pol-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-proofs-error", "logos-blockchain-utils", - "logos-blockchain-witness-generator", "num-bigint 0.4.6", "num-traits", "serde", @@ -5847,14 +5986,15 @@ dependencies = [ [[package]] name = "logos-blockchain-poq" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-poq-sys", "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-pol", "logos-blockchain-proofs-error", - "logos-blockchain-witness-generator", "num-bigint 0.4.6", "serde", "serde_json", @@ -5865,7 +6005,7 @@ dependencies = [ [[package]] name = "logos-blockchain-poseidon2" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-bn254 0.4.0", "ark-ff 0.4.2", @@ -5876,8 +6016,9 @@ dependencies = [ [[package]] name = "logos-blockchain-proofs-error" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ + "logos-blockchain-circuits-types", "logos-blockchain-groth16", "serde_json", "thiserror 2.0.18", @@ -5886,7 +6027,7 @@ dependencies = [ [[package]] name = "logos-blockchain-services-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -5901,7 +6042,7 @@ dependencies = [ [[package]] name = "logos-blockchain-storage-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "bytes", @@ -5920,7 +6061,7 @@ dependencies = [ [[package]] name = "logos-blockchain-time-service" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -5943,7 +6084,7 @@ dependencies = [ [[package]] name = "logos-blockchain-tracing" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "flate2", "logos-blockchain-log-targets", @@ -5969,7 +6110,7 @@ dependencies = [ [[package]] name = "logos-blockchain-utils" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "blake2", @@ -5988,7 +6129,7 @@ dependencies = [ [[package]] name = "logos-blockchain-utxotree" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "ark-ff 0.4.2", "logos-blockchain-groth16", @@ -5999,25 +6140,18 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "logos-blockchain-witness-generator" -version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" -dependencies = [ - "tempfile", -] - [[package]] name = "logos-blockchain-zksign" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "logos-blockchain-circuits-prover", + "logos-blockchain-circuits-signature-sys", + "logos-blockchain-circuits-types", "logos-blockchain-circuits-utils", "logos-blockchain-groth16", "logos-blockchain-poseidon2", "logos-blockchain-proofs-error", - "logos-blockchain-witness-generator", "num-bigint 0.4.6", "serde", "serde_json", @@ -6028,7 +6162,7 @@ dependencies = [ [[package]] name = "logos-blockchain-zone-sdk" version = "0.1.2" -source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=dd055cc1ef7c130f710a52a190edd97bc7b0f71b#dd055cc1ef7c130f710a52a190edd97bc7b0f71b" +source = "git+https://github.com/logos-blockchain/logos-blockchain.git?rev=db9a8d821c1b20f29b03d02072817150cf969b8e#db9a8d821c1b20f29b03d02072817150cf969b8e" dependencies = [ "async-trait", "futures", @@ -6231,15 +6365,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mempool" version = "0.1.0" @@ -6254,7 +6379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" dependencies = [ "byteorder", - "keccak", + "keccak 0.1.6", "rand_core 0.6.4", "zeroize", ] @@ -6265,7 +6390,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -6317,6 +6442,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-kem" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" +dependencies = [ + "hybrid-array", + "kem", + "module-lattice", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha3", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "moka" version = "0.12.15" @@ -6508,7 +6658,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "libc", "log", "netlink-packet-core 0.8.1", @@ -6568,10 +6718,10 @@ dependencies = [ "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-serialize 0.4.2", - "digest", + "digest 0.10.7", "generic-array 0.14.7", "hex", - "keccak", + "keccak 0.1.6", "log", "rand 0.8.6", "zeroize", @@ -6583,7 +6733,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", @@ -6840,7 +6990,7 @@ version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "foreign-types 0.3.2", "libc", @@ -7158,9 +7308,9 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -7169,8 +7319,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -7452,7 +7612,7 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "num-traits", "rand 0.9.4", "rand_chacha 0.9.0", @@ -7540,37 +7700,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -7578,9 +7733,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -7590,13 +7745,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn 2.0.117", ] @@ -7676,7 +7830,7 @@ dependencies = [ "once_cell", "socket2 0.6.4", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7912,7 +8066,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -8127,7 +8281,7 @@ dependencies = [ "anyhow", "bytemuck", "cfg-if", - "keccak", + "keccak 0.1.6", "liblzma", "paste", "rayon", @@ -8310,7 +8464,7 @@ dependencies = [ "borsh", "bytemuck", "cfg-if", - "digest", + "digest 0.10.7", "ff", "hex", "hex-literal 0.4.1", @@ -8349,7 +8503,7 @@ dependencies = [ "gdbstub_arch", "gimli", "hex", - "keccak", + "keccak 0.1.6", "lazy-regex", "num-bigint 0.4.6", "num-traits", @@ -8413,9 +8567,9 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "rpassword" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a57a69104632d64deb0df2e09a69945cd7a6eab4070fc9b1d7e50cf6c3edc" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", @@ -8468,16 +8622,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", "signature", - "spki", + "spki 0.7.3", "subtle", "zeroize", ] @@ -8583,7 +8737,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -8607,9 +8761,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -8793,9 +8947,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.10", "generic-array 0.14.7", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "zeroize", @@ -8807,7 +8961,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -8865,6 +9019,7 @@ dependencies = [ "logos-blockchain-zone-sdk", "mempool", "rand 0.8.6", + "risc0-zkvm", "serde", "serde_json", "storage", @@ -9178,7 +9333,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -9189,7 +9344,17 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", ] [[package]] @@ -9207,6 +9372,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -9223,7 +9394,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -9322,7 +9493,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -9481,7 +9662,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -9492,7 +9673,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -9551,6 +9732,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -10109,7 +10301,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-core", "futures-util", @@ -10372,9 +10564,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "typetag" @@ -10441,9 +10633,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -10457,12 +10649,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "unit-prefix" version = "0.5.2" @@ -10510,12 +10696,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64", + "flate2", "log", "percent-encoding", "rustls", "rustls-pki-types", "ureq-proto", "utf8-zero", + "webpki-roots", ] [[package]] @@ -10599,9 +10787,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -10899,7 +11087,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -11138,15 +11326,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -11180,30 +11359,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -11225,12 +11387,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -11243,12 +11399,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -11261,24 +11411,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -11291,12 +11429,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -11309,12 +11441,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -11327,12 +11453,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -11345,12 +11465,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" @@ -11433,7 +11547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap 2.14.0", "log", "serde", @@ -11589,18 +11703,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 7a6a37ad..ae2f05d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,15 +137,14 @@ url = { version = "2.5.4", features = ["serde"] } tokio-retry = "0.3.0" schemars = "1.2" async-stream = "0.3.6" -criterion = { version = "0.8", features = ["html_reports"] } -logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } -logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "dd055cc1ef7c130f710a52a190edd97bc7b0f71b" } +logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-core = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-chain-broadcast-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } +logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "db9a8d821c1b20f29b03d02072817150cf969b8e" } rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", @@ -159,13 +158,16 @@ k256 = { version = "0.13.3", features = [ "serde", "pem", ] } +ml-kem = { version = "0.3", features = ["hazmat"] } elliptic-curve = { version = "0.13.8", features = ["arithmetic"] } actix-web = { version = "4.13.0", default-features = false, features = [ "macros", ] } clap = { version = "4.5.42", features = ["derive", "env"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } -pyo3 = { version = "0.24", features = ["auto-initialize"] } +pyo3 = { version = "0.29", features = ["auto-initialize"] } +zeroize = "1" +criterion = { version = "0.8", features = ["html_reports"] } # Profile for leptos WASM release builds [profile.wasm-release] diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index a3e841c8..317fd705 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 750294f7..dd841336 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 20360688..ec15731b 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/bridge.bin b/artifacts/program_methods/bridge.bin index bd30da09..69c18210 100644 Binary files a/artifacts/program_methods/bridge.bin and b/artifacts/program_methods/bridge.bin differ diff --git a/artifacts/program_methods/clock.bin b/artifacts/program_methods/clock.bin index 452784ba..ddbfea9b 100644 Binary files a/artifacts/program_methods/clock.bin and b/artifacts/program_methods/clock.bin differ diff --git a/artifacts/program_methods/faucet.bin b/artifacts/program_methods/faucet.bin index 4ad52fb9..4994d29e 100644 Binary files a/artifacts/program_methods/faucet.bin and b/artifacts/program_methods/faucet.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index b15aa385..84ea7e23 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 e28ff793..cb240b78 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 f1317ebc..a7f6d3e9 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 074dda6b..59989832 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/program_methods/vault.bin b/artifacts/program_methods/vault.bin index 5cfa89d4..81e79348 100644 Binary files a/artifacts/program_methods/vault.bin and b/artifacts/program_methods/vault.bin differ diff --git a/artifacts/test_program_methods/auth_asserting_noop.bin b/artifacts/test_program_methods/auth_asserting_noop.bin index 3ead7c7b..0e9e0dc1 100644 Binary files a/artifacts/test_program_methods/auth_asserting_noop.bin and b/artifacts/test_program_methods/auth_asserting_noop.bin differ diff --git a/artifacts/test_program_methods/auth_transfer_proxy.bin b/artifacts/test_program_methods/auth_transfer_proxy.bin index 26f4df47..c5863615 100644 Binary files a/artifacts/test_program_methods/auth_transfer_proxy.bin and b/artifacts/test_program_methods/auth_transfer_proxy.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index 514a39f4..c7684bfc 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 dc0a4992..39b6cb2f 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 e2639c45..d1f2cb8a 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 99d1301c..035fdb23 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 fd0f52ef..b9406338 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 31689af4..42429550 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 121a7a29..9685f34f 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/faucet_chain_caller.bin b/artifacts/test_program_methods/faucet_chain_caller.bin index faa150dd..b72df852 100644 Binary files a/artifacts/test_program_methods/faucet_chain_caller.bin and b/artifacts/test_program_methods/faucet_chain_caller.bin differ diff --git a/artifacts/test_program_methods/flash_swap_callback.bin b/artifacts/test_program_methods/flash_swap_callback.bin index 1e4e74f8..2f9020c9 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 2ae1e845..6ef7044b 100644 Binary files a/artifacts/test_program_methods/flash_swap_initiator.bin and b/artifacts/test_program_methods/flash_swap_initiator.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 20878083..37716f90 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 2a1358a0..09507957 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_injector.bin b/artifacts/test_program_methods/malicious_injector.bin index 0f3cbfd7..cb90bbc5 100644 Binary files a/artifacts/test_program_methods/malicious_injector.bin and b/artifacts/test_program_methods/malicious_injector.bin differ diff --git a/artifacts/test_program_methods/malicious_launderer.bin b/artifacts/test_program_methods/malicious_launderer.bin index e8932cab..368602ca 100644 Binary files a/artifacts/test_program_methods/malicious_launderer.bin and b/artifacts/test_program_methods/malicious_launderer.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 91f88c58..dd0343d0 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 5064f754..7bf2b4ed 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 11a34838..6bac61a9 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 1f47a878..83a6c3b8 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 4d5ddb34..45fd8ea8 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 4b59b8b7..4c8248e0 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 6ac0067d..5dc4031e 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 index 03e85c2e..9a04a29c 100644 Binary files a/artifacts/test_program_methods/pda_fund_spend_proxy.bin and b/artifacts/test_program_methods/pda_fund_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pda_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin index 8f5641e6..6b6e6a41 100644 Binary files a/artifacts/test_program_methods/pda_spend_proxy.bin and b/artifacts/test_program_methods/pda_spend_proxy.bin differ diff --git a/artifacts/test_program_methods/pinata_cooldown.bin b/artifacts/test_program_methods/pinata_cooldown.bin index 4f52bcdf..d807c71c 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_delegator.bin b/artifacts/test_program_methods/private_pda_delegator.bin index e3370b50..5fb95519 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/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index b49681a3..44ab9808 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 4d5bb4d8..73f85aef 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 7606f56c..4955beb3 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 ebc6e2ae..15c990c2 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 d2e203a5..65aa5988 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 1958ee4d..c13a3810 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/docs/LEZ testnet v0.1 tutorials/keycard.md b/docs/LEZ testnet v0.1 tutorials/keycard.md index 81b154e3..47573f12 100644 --- a/docs/LEZ testnet v0.1 tutorials/keycard.md +++ b/docs/LEZ testnet v0.1 tutorials/keycard.md @@ -60,17 +60,44 @@ Unset it when done: unset KEYCARD_PIN ``` +## Pairing password + +The pairing password is used to establish a secure channel between the wallet and the card. It is set permanently on the card during `wallet keycard init` and must match on every subsequent re-pair. + +The default password (`KeycardDefaultPairing`) is [recommended](https://docs.keycard.tech/en/developers/core) for most users. Wallet CLI allows advance users the flexibility to set their own pairing password. + +To use a custom pairing password, set it before `init`: + +```bash +# Note: Keep the leading space before this command. +# Leading space prevents this command from being stored in shell history +# (when HISTCONTROL=ignorespace is enabled). + export KEYCARD_PAIRING_PASSWORD=my-custom-password +wallet keycard init +``` + +After a successful initializaation, subsequent commands (`connect`, transfers) use the cached pairing index and key — the pairing password is not needed again until the pairing is cleared. + +**Important:** if you initialized with a custom password, `KEYCARD_PAIRING_PASSWORD` must be set in every session where re-pairing can occur (after `disconnect`, or on a new machine). If the env var is missing then wallet CLI will attempt to use the default password. As a result, pairing will fail. + +Unset the pairing password variable when done: + +```bash +unset KEYCARD_PAIRING_PASSWORD +``` + ## Keycard Commands ### Keycard -| Command | Description | -|-----------------------------|------------------------------------------------------------| -| `wallet keycard available` | Checks whether a Keycard reader and card are accessible | -| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK | -| `wallet keycard connect` | Establishes and saves a pairing with the Keycard | -| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing | -| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard | +| Command | Description | +|----------------------------------|-----------------------------------------------------------------------| +| `wallet keycard available` | Checks whether a Keycard reader and card are accessible | +| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK | +| `wallet keycard connect` | Establishes and saves a pairing with the Keycard | +| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing | +| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard | +| `wallet keycard get-private-keys`| Prints NSK and VSK for a BIP-32 path — **debug builds only** (see below) | 1. Check keycard availability ```bash @@ -122,6 +149,31 @@ Keycard PIN: ✅ Keycard unpaired and pairing cleared. ``` +6. Get private keys for a BIP-32 path (**debug builds only**) + +`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. The command is only available in debug builds and requires `--reveal` to confirm intent. + +First install the wallet with the `keycard-debug` feature: +```bash +cargo install --path lez/wallet --force --features keycard-debug +``` + +Then run the command: +```bash +wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" --reveal + +# Output: +WARNING: NSK and VSK are being printed to stdout. Any terminal log, scrollback, or screen recording captures these keys. +Keycard PIN: +NSK: 55e505bf925e536c843a12ebc08c41ca5f4761eeeb7fa33725f0b44e6f1ac2e4 +VSK: 30f798893977a7b7263d1f77abf58e11e014428c92030d6a02fe363cceb41ffa +``` + +To restore the standard build without `keycard-debug` afterwards: +```bash +cargo install --path lez/wallet --force +``` + ### Pinata (testnet) | Command | Description | @@ -213,25 +265,270 @@ Keycard PIN: Transaction hash is 7d4c1b8e2f903a56fd19084b3c8b25d07e8f243829bc50addf6e2c78b4b09e45 ``` +### Token program + +`--definition`, `--holder`, `--from`, and `--to` each accept any of: +- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`) +- An account ID with privacy prefix (e.g. `Public/9bKm...`) +- An account label (e.g. `my-account`) + +The token program requires both the definition account and the holder/recipient to sign when both are owned. If only one is a Keycard path, only that account signs via the card; the other signs locally or is treated as foreign. + +**Shielded transfers** (public Keycard sender → private recipient) are supported. The Keycard signs the public sender's authorization; the ZK circuit handles the private recipient side. + +| Command | Description | +|--------------------|-------------------------------------------------------| +| `wallet token new` | Creates a new token definition with an initial supply | +| `wallet token send`| Transfers tokens between accounts | +| `wallet token mint`| Mints tokens to a holder account | +| `wallet token burn`| Burns tokens from a holder account | + +1. Create a new token — definition and supply both on Keycard +```bash +wallet token new \ + --definition-account-id "m/44'/60'/0'/0/2" \ + --supply-account-id "m/44'/60'/0'/0/3" \ + --name LEZ \ + --total-supply 100000 + +# Output: +Keycard PIN: +Transaction hash is a3f1c8e2049b7d56fe19084b3c8b25d07e8f243829bc50addf6e2c78b4b09d11 +Transaction data is ... +``` + +2. Transfer tokens between two Keycard accounts (public → public) +```bash +wallet token send \ + --from "m/44'/60'/0'/0/3" \ + --to "m/44'/60'/0'/0/6" \ + --amount 20000 + +# Output: +Keycard PIN: +Transaction hash is b2e4d9f1038c6e45ad28175c4d9c36e18bf9354930cd61beef59f3e89c5a0e22 +Transaction data is ... +``` + +3. Transfer tokens from a Keycard account to a private account (shielded) +```bash +wallet token send \ + --from "m/44'/60'/0'/0/6" \ + --to "Private/CJwKfrb3DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7Rb" \ + --amount 500 + +# Output: +Keycard PIN: +Transaction hash is c5f7e0a2149d8f67be39286d5eaa47f29cg0465041de72cff06a4f9ad6b1f33 +``` + +4. Mint tokens — Keycard definition account mints to a Keycard holder +```bash +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 2000 + +# Output: +Keycard PIN: +Transaction hash is d6g8f1b3250e9a78cf4a397e6fbb58g3ah1567152ef83dgg17b5g0be7c2g0g44 +Transaction data is ... +``` + +5. Burn tokens — Keycard holder burns from its own account +```bash +wallet token burn \ + --definition "Public/9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 500 + +# Output: +Keycard PIN: +Transaction hash is e7h9g2c4361f0b89dg5b408f7gcc69h4bi2678263fg94ehh28c6h1cf8d3h1h55 +Transaction data is ... +``` + +### AMM program + +AMM operations are **public only** — all holdings involved must be public accounts. Keycard accounts can be used for any or all of the holding accounts. + +`--user-holding-a`, `--user-holding-b`, and `--user-holding-lp` each accept any of: +- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`) +- An account ID with privacy prefix (e.g. `Public/9bKm...`) +- An account label (e.g. `my-account`) + +For swaps, only the seller's holding signs — the wallet identifies which holding corresponds to the input token and signs only that account. + +| Command | Description | +|----------------------------|-------------------------------------------------------| +| `wallet amm new` | Creates a new AMM liquidity pool | +| `wallet amm swap-exact-input` | Swaps specifying exact input amount | +| `wallet amm swap-exact-output` | Swaps specifying exact output amount | +| `wallet amm add-liquidity` | Adds liquidity to an existing pool | +| `wallet amm remove-liquidity` | Removes liquidity from a pool | + +1. Create a new AMM pool — all holdings on Keycard +```bash +wallet amm new \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ + --balance-a 10000 \ + --balance-b 10000 + +# Output: +Keycard PIN: +Transaction hash is f8i0h3d5472g1c90eh6c519g8hdd70i5cj3789374gh05fii39d7i2dg9e4i2i66 +Transaction data is ... +``` + +2. Swap exact input — Keycard account sells LEE, receives LEZ +```bash +wallet amm swap-exact-input \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --amount-in 500 \ + --min-amount-out 1 \ + --token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" + +# Output: +Keycard PIN: +Transaction hash is g9j1i4e6583h2d01fi7d620h9iee81j6dk4890485hi16gjj40e8j3eh0f5j3j77 +Transaction data is ... +``` + +3. Add liquidity — all three holdings on Keycard +```bash +wallet amm add-liquidity \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ + --max-amount-a 1000 \ + --max-amount-b 1000 \ + --min-amount-lp 1 + +# Output: +Keycard PIN: +Transaction hash is h0k2j5f7694i3e12gj8e731i0jff92k7el5901596ij27hkk51f9k4fi1g6k4k88 +Transaction data is ... +``` + +4. Remove liquidity — LP holding on Keycard +```bash +wallet amm remove-liquidity \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ + --balance-lp 500 \ + --min-amount-a 1 \ + --min-amount-b 1 + +# Output: +Keycard PIN: +Transaction hash is i1l3k6g8705j4f23hk9f842j1kgg03l8fm6012607jk38ill62g0l5gj2h7l5l99 +Transaction data is ... +``` + +### ATA program + +The Associated Token Account program derives a deterministic token holding address from an owner account and a token definition. Keycard accounts can be used as the owner. + +`--owner` and `--from`/`--holder` accept any of: +- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`) +- An account ID with privacy prefix (e.g. `Public/9bKm...`) +- An account label (e.g. `my-account`) + +| Command | Description | +|--------------------|------------------------------------------------------------------| +| `wallet ata address` | Derives and prints the ATA address (local only, no network) | +| `wallet ata create` | Creates the ATA on-chain | +| `wallet ata send` | Sends tokens from the owner's ATA to a recipient | +| `wallet ata burn` | Burns tokens from the owner's ATA | +| `wallet ata list` | Lists ATAs for a given owner across token definitions | + +1. Derive an ATA address for a Keycard account +```bash +# First resolve the Keycard account ID +OWNER_ID=$(wallet account id --account-id "m/44'/60'/0'/0/9") +wallet ata address \ + --owner "$OWNER_ID" \ + --token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" + +# Output: +DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7RbCJwKfrb3 +``` + +2. Create an ATA — Keycard account as owner +```bash +wallet ata create \ + --owner "m/44'/60'/0'/0/9" \ + --token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" + +# Output: +Keycard PIN: +Transaction hash is j2m4l7h9816k5g34il0g953k2lhh14m9gn7123718kl49jmm73h1m6hk3i8m6m00 +Transaction data is ... +``` + +3. Send tokens from a Keycard ATA to another account +```bash +wallet ata send \ + --from "m/44'/60'/0'/0/9" \ + --token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \ + --to "DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7RbCJwKfrb3" \ + --amount 500 + +# Output: +Keycard PIN: +Transaction hash is k3n5m8i0927l6h45jm1h064l3mii25n0ho8234829lm50knn84i2n7il4j9n7n11 +Transaction data is ... +``` + +4. Burn tokens from a Keycard ATA +```bash +wallet ata burn \ + --holder "m/44'/60'/0'/0/9" \ + --token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \ + --amount 200 + +# Output: +Keycard PIN: +Transaction hash is l4o6n9j1038m7i56kn2i175m4njj36o1ip9345930mn61loo95j3o8jm5k0o8o22 +Transaction data is ... +``` + ## Testing -Tests for Keycard commands are in `lez/keycard_wallet/tests/keycard_tests.sh`. Run from the repo root with a Keycard connected: +Tests for Keycard commands are in `lez/keycard_wallet/tests/`. + +| Test file | Description | +|---|---| +| `keycard_tests.sh` | Core Keycard wallet commands and `auth-transfer` commands | +| `keycard_tests_2.sh` | Tests Keycard wallet commands for `amma`, `token` and `ata` programs | +| `keycard_test_3.sh` | Demonstrates retrieving private account keys from keycard | +| `keycard_power_recovery_tests.sh` | Modified test file of `keycard_tests.sh` to test power recovery paths | + +Run from the repo root with a Keycard connected: ```bash bash lez/keycard_wallet/tests/keycard_tests.sh +bash lez/keycard_wallet/tests/keycard_tests_2.sh +bash lez/keycard_wallet/tests/keycard_test_3.sh +bash lez/keycard_wallet/tests/keycard_power_recovery_tests.sh ``` -## SigningGroups +## SigningGroup -`SigningGroups` (`wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved. +`SigningGroup` (`lez/wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved. Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing. Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce. ``` -SigningGroups { +SigningGroup { local: [(AccountId, PrivateKey)], // signed in pure Rust keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session } +``` ``` \ No newline at end of file diff --git a/docs/LEZ testnet v0.1 tutorials/token-transfer.md b/docs/LEZ testnet v0.1 tutorials/token-transfer.md index 3a1ef43f..e3d04663 100644 --- a/docs/LEZ testnet v0.1 tutorials/token-transfer.md +++ b/docs/LEZ testnet v0.1 tutorials/token-transfer.md @@ -155,7 +155,7 @@ wallet account new private # Output: Generated new account with account_id Private/HacPU3hakLYzWtSqUPw6TUr8fqoMieVWovsUR6sJf7cL With npk e6366f79d026c8bd64ae6b3d601f0506832ec682ab54897f205fffe64ec0d951 -With vpk 02ddc96d0eb56e00ce14994cfdaec5ae1f76244180a919545983156e3519940a17 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` > [!Tip] @@ -231,19 +231,29 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /1 With npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e -With vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` -> [!Tip] -> Ignore the account ID here and use the `npk` and `vpk` values to send to a foreign private account. +> [!Important] +> The VPK is now a 1184-byte ML-KEM-768 encapsulation key — too large to copy-paste into a command. +> The recommended workflow is: +> +> **Recipient:** export both keys to a single file and send the file to the sender (e.g. as an email attachment): +> ```bash +> wallet account show-keys --account-id Private/ > recipient.keys +> # Send recipient.keys to the sender out-of-band +> ``` +> The file contains two lines: the npk (hex) on line 1, the vpk (hex) on line 2. +> +> **Sender:** reference the received file with `--to-keys`: -### b. Send 3 tokens using the recipient’s npk and vpk +### b. Send 3 tokens using the recipient’s keys file ```bash +# The sender has received recipient.keys from the recipient out-of-band wallet auth-transfer send \ --from Public/Ev1JprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPAWS \ - --to-npk 0c95ebc4b3830f53da77bb0b80a276a776cdcf6410932acc718dcdb3f788a00e \ - --to-vpk 039fd12a3674a880d3e917804129141e4170d419d1f9e28a3dcf979c1f2369cb72 \ + --to-keys recipient.keys \ --amount 3 ``` @@ -270,18 +280,19 @@ wallet account new private-accounts-key # Output: Generated new private accounts key at path /2 With npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 -With vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c +With vpk <1184-byte ML-KEM-768 encapsulation key, hex-encoded> ``` Alice shares the `npk` and `vpk` values with Bob and Charlie out of band. ### b. Bob sends 10 tokens to Alice using identifier 1 +Bob uses the received `alice.keys` file: + ```bash wallet auth-transfer send \ --from Public/BobXqJprP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPA \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 1 \ --amount 10 ``` @@ -291,8 +302,7 @@ wallet auth-transfer send \ ```bash wallet auth-transfer send \ --from Public/CharlieYrP9BmhbFVQyBcbznU8bAXcwrzwRoPTetXdQPB \ - --to-npk a3f7c21b8e905d4f6a1bc783d0e2f94c1d5a6b7e8f9012345678abcdef012345 \ - --to-vpk 03b1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6071819202122232425262728292a2b2c \ + --to-keys alice.keys \ --to-identifier 2 \ --amount 5 ``` diff --git a/docs/benchmarks/cycle_bench.md b/docs/benchmarks/cycle_bench.md index 0e880070..2a8785f0 100644 --- a/docs/benchmarks/cycle_bench.md +++ b/docs/benchmarks/cycle_bench.md @@ -14,21 +14,37 @@ Per-program Risc0 cycle counts, prover wall time, PPE composition cost, and veri | Profile | release | | GPU acceleration | none | -## Executor cycles +## Executor cycles and public-execution ms -`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over 5 timed iterations (1 warmup discarded). +`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over the timed iterations (1 warmup discarded; `--exec-iters` sets the count, 50 below). `calib_ms` and `net_ms` are the public-execution time in milliseconds, on the same axis as the private `G_verify` so the fee model has one common unit for both paths. See the calibration block below for how they are derived. -| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) | -|---|---|---:|---:|---| -| authenticated_transfer | Initialize | 43,642 | 1 | 18.86 / 19.41 ± 0.48 | -| authenticated_transfer | Transfer | 77,095 | 1 | 19.67 / 20.84 ± 1.16 | -| token | Burn | 116,546 | 1 | 24.86 / 25.46 ± 0.63 | -| token | Mint | 116,862 | 1 | 24.47 / 25.08 ± 0.42 | -| token | Transfer | 127,726 | 1 | 25.00 / 25.40 ± 0.29 | -| clock | Tick (no rollups) | 137,022 | 1 | 21.18 / 21.57 ± 0.41 | -| ata | Create | 175,056 | 1 | 23.64 / 24.94 ± 1.09 | -| amm | SwapExactInput | 508,634 | 1 | 34.21 / 34.77 ± 0.55 | -| amm | AddLiquidity | 642,774 | 1 | 37.59 / 37.87 ± 0.28 | +| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) | calib_ms | net_ms | +|---|---|---:|---:|---|---:|---:| +| authenticated_transfer | Initialize | 43,818 | 1 | 30.69 / 31.93 ± 1.03 | 1.31 | 0.29 | +| authenticated_transfer | Transfer | 79,958 | 1 | 31.02 / 32.35 ± 0.59 | 2.38 | 0.61 | +| token | Burn | 116,546 | 1 | 36.08 / 37.18 ± 0.60 | 3.47 | 5.67 | +| token | Mint | 116,862 | 1 | 35.67 / 37.73 ± 2.54 | 3.48 | 5.26 | +| token | Transfer | 127,726 | 1 | 35.49 / 36.86 ± 0.90 | 3.81 | 5.08 | +| clock | Tick (no rollups) | 137,022 | 1 | 32.12 / 33.16 ± 0.89 | 4.08 | 1.72 | +| ata | Create | 174,515 | 1 | 35.41 / 36.49 ± 0.65 | 5.20 | 5.00 | +| amm | SwapExactInput | 508,904 | 1 | 46.71 / 48.06 ± 0.86 | 15.17 | 16.30 | +| amm | AddLiquidity | 643,464 | 1 | 48.57 / 50.28 ± 0.98 | 19.18 | 18.16 | + +### Public-execution ms calibration + +The binary fits `best_ms = intercept + slope · user_cycles` by ordinary least squares across the nine cases (best-of-N, not mean, so one OS scheduling spike cannot tilt the slope). On the machine above: + +| Field | Value | +|---|---| +| throughput (1 / slope) | 33,546 cycles/ms | +| fixed overhead (intercept) | 30.41 ms per call | +| R² | 0.935 | + +- `calib_ms = user_cycles / throughput` is the compute-only time, a pure function of the deterministic cycle count and the one pinned-hardware constant, so it reproduces run to run where raw wall-time does not. This is the number to put on the common public/private ms axis. +- `net_ms = best exec_ms − fixed overhead` is the measured compute with the host-side overhead stripped; it agrees with `calib_ms` to within the per-program overhead scatter (the intercept is an ELF-size-averaged constant, so this decomposition is first-order, not mechanistic). +- The `fixed overhead` is host-side per-call setup (ELF parse into a `MemoryImage`, `ExecutorEnv` build) that is outside the cycle count and does not scale with the instruction's work. + +The fixed overhead is paid per transaction in the current node, not amortized. The public-execution path at `lee/state_machine/src/program.rs:56-87` builds a fresh `ExecutorEnv` and calls `default_executor().execute(env, self.elf())` per call with the raw ELF bytes; no parsed image is cached across transactions. So today the real per-public-tx sequencer cost is the raw `exec_ms` (≈ 31 ms for the cheapest program), overhead-dominated. Caching the parsed `MemoryImage` per `ProgramId` would drop the per-tx cost to `calib_ms` (1–19 ms). Public execution is also cycle-capped at `MAX_NUM_CYCLES_PUBLIC_EXECUTION` (`program.rs:64`), which bounds the worst-case public-tx cost. ## Real proving (`--prove`) @@ -85,7 +101,8 @@ The corresponding `proof_bytes` (S_agg) for the bench receipt is captured by `-- ## Reproduce ```sh -cargo run --release -p cycle_bench +# Executor cycles + public-execution ms calibration (no proving). --exec-iters sets the sample count. +cargo run --release -p cycle_bench -- --exec-iters 50 cargo run --release -p cycle_bench --features prove -- --prove cargo run --release -p cycle_bench --features ppe -- --prove --ppe diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index b7a747f1..9f953001 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -132,6 +132,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -158,6 +159,7 @@ async fn amm_public() -> Result<()> { to: Some(public_mention(recipient_account_id_2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 7, }; @@ -530,6 +532,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_a_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; @@ -551,6 +554,7 @@ async fn amm_new_pool_using_labels() -> Result<()> { to: Some(public_mention(holding_b_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 5, }; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 5b4a6c0b..9e37061b 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -260,6 +260,7 @@ async fn transfer_and_burn_via_ata() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -487,6 +488,7 @@ async fn transfer_via_ata_private_owner() -> Result<()> { to: Some(public_mention(sender_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), @@ -598,6 +600,7 @@ async fn burn_via_ata_private_owner() -> Result<()> { to: Some(public_mention(holder_ata_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: fund_amount, }), diff --git a/integration_tests/tests/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index ad3e5b18..45a1b085 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,8 +11,9 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata, - encryption::shared_key_derivation::Secp256k1Point, + EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, + account::AccountWithMetadata, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; use log::info; use sequencer_service_rpc::RpcClient as _; @@ -38,6 +39,7 @@ async fn private_transfer_to_owned_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -71,13 +73,14 @@ async fn private_transfer_to_foreign_account() -> Result<()> { let from: AccountId = ctx.existing_private_accounts()[0]; let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: private_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -127,6 +130,7 @@ async fn deshielded_transfer_to_public_account() -> Result<()> { to: Some(public_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -189,7 +193,8 @@ async fn private_transfer_to_owned_account_using_claiming_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -239,6 +244,7 @@ async fn shielded_transfer_to_owned_private_account() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -274,14 +280,15 @@ async fn shielded_transfer_to_foreign_account() -> Result<()> { let to_npk = NullifierPublicKey([42; 32]); let to_npk_string = hex::encode(to_npk.0); - let to_vpk = Secp256k1Point::from_scalar(to_npk.0); + let to_vpk = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let from: AccountId = ctx.existing_public_accounts()[0]; let command = Command::AuthTransfer(AuthTransferSubcommand::Send { from: public_mention(from), to: None, to_npk: Some(to_npk_string), - to_vpk: Some(hex::encode(to_vpk.0)), + to_vpk: Some(hex::encode(to_vpk.to_bytes())), + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -351,7 +358,8 @@ async fn private_transfer_to_owned_account_continuous_run_path() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode(to.key_chain.viewing_public_key.to_bytes())), + to_keys: None, to_identifier: Some(to.kind.identifier()), amount: 100, }); @@ -452,6 +460,7 @@ async fn private_transfer_using_from_label() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -545,7 +554,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { }; let npk_hex = hex::encode(npk.0); - let vpk_hex = hex::encode(vpk.0); + let vpk_hex = hex::encode(vpk.to_bytes()); let identifier_1 = 1_u128; let identifier_2 = 2_u128; @@ -560,6 +569,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex.clone()), to_vpk: Some(vpk_hex.clone()), + to_keys: None, to_identifier: Some(identifier_1), amount: 100, }), @@ -573,6 +583,7 @@ async fn shielded_transfers_to_two_identifiers_same_npk() -> Result<()> { to: None, to_npk: Some(npk_hex), to_vpk: Some(vpk_hex), + to_keys: None, to_identifier: Some(identifier_2), amount: 200, }), @@ -654,8 +665,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { let auth_transfer_program_id = Program::authenticated_transfer_program().id(); let nsk: lee_core::NullifierSecretKey = [3; 32]; let npk = NullifierPublicKey::from(&nsk); - let vpk = Secp256k1Point::from_scalar([4; 32]); - let ssk = SharedSecretKey::new([55; 32], &vpk); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let ssk = SharedSecretKey([55_u8; 32]); + let epk = EphemeralPublicKey(vec![55_u8; 1088]); let attacker_vault_id = { let seed = vault_core::compute_vault_seed(attacker_id); AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337) @@ -700,6 +712,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { + epk, + view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), npk, ssk, identifier: 1337, diff --git a/integration_tests/tests/auth_transfer/public.rs b/integration_tests/tests/auth_transfer/public.rs index cd19e55d..d00b7964 100644 --- a/integration_tests/tests/auth_transfer/public.rs +++ b/integration_tests/tests/auth_transfer/public.rs @@ -25,6 +25,7 @@ async fn successful_transfer_to_existing_account() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -83,6 +84,7 @@ pub async fn successful_transfer_to_new_account() -> Result<()> { to: Some(public_mention(new_persistent_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -120,6 +122,7 @@ async fn failed_transfer_with_insufficient_balance() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000_000, }); @@ -159,6 +162,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -192,6 +196,7 @@ async fn two_consecutive_successful_transfers() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -274,6 +279,7 @@ async fn successful_transfer_using_from_label() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -319,6 +325,7 @@ async fn successful_transfer_using_to_label() -> Result<()> { to: Some(CliAccountMention::Label(label)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/bridge.rs b/integration_tests/tests/bridge.rs index 72157952..e7d52e83 100644 --- a/integration_tests/tests/bridge.rs +++ b/integration_tests/tests/bridge.rs @@ -4,12 +4,14 @@ reason = "We don't care about these in tests" )] -use std::time::Duration; +use std::{ops::Deref as _, time::Duration}; use anyhow::Context as _; use borsh::BorshSerialize; use common::transaction::LeeTransaction; -use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext}; +use integration_tests::{ + TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, wait_for_indexer_to_catch_up, +}; use lee::{ AccountId, execute_and_prove, privacy_preserving_transaction, program::Program, public_transaction, @@ -43,6 +45,7 @@ async fn public_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { vec![bridge_account_id, recipient_vault_id], vec![], bridge_core::Instruction::Deposit { + l1_deposit_op_id: [0_u8; 32], vault_program_id, recipient_id, amount: 1, @@ -129,6 +132,7 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { // Serialize the bridge deposit instruction let instruction = Program::serialize_instruction(bridge_core::Instruction::Deposit { + l1_deposit_op_id: [0_u8; 32], vault_program_id, recipient_id, amount: 1, @@ -148,7 +152,6 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> { let message = privacy_preserving_transaction::Message::try_from_circuit_output( vec![bridge_account_id, recipient_vault_id], vec![bridge_pre.account.nonce, vault_pre.account.nonce], - vec![], output, ) .context("Failed to build privacy-preserving bridge deposit message")?; @@ -204,7 +207,9 @@ async fn submit_bedrock_deposit( // Encode deposit metadata let metadata = borsh::to_vec(&DepositMetadata { recipient_id }) - .context("Failed to encode deposit metadata")?; + .context("Failed to encode deposit metadata")? + .try_into() + .context("Encoded metadata is too big")?; let funding_key = "2e03b2eff5a45478e7e79668d2a146cf2c5c7925bce927f2b1c67f2ab4fc0d26"; @@ -307,7 +312,7 @@ async fn submit_bedrock_deposit( tip: None, deposit: DepositOp { channel_id, - inputs: Inputs::new(vec![selected_note_id]), + inputs: Inputs::new(selected_note_id), metadata, }, change_public_key: balance.address, @@ -446,5 +451,26 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result< "Recipient balance should increase by claimed amount" ); + // The indexer must replay the deposit and claim blocks and reach the same + // state as the sequencer — including the bridge system account the deposit + // modifies, which is the case the hot fix unblocks. + wait_for_indexer_to_catch_up(&ctx).await?; + let bridge_account_id = lee::system_bridge_account_id(); + for account_id in [recipient_id, recipient_vault_id, bridge_account_id] { + let indexer_account = indexer_service_rpc::RpcClient::get_account( + // `deref` is needed for correct trait resolution + // of the async `get_account` method on `RpcClient` + ctx.indexer_client().deref(), + account_id.into(), + ) + .await?; + let sequencer_account = ctx.sequencer_client().get_account(account_id).await?; + assert_eq!( + indexer_account, + sequencer_account.into(), + "Indexer and sequencer diverged for account {account_id} after deposit" + ); + } + Ok(()) } diff --git a/integration_tests/tests/indexer_ffi_state_consistency.rs b/integration_tests/tests/indexer_ffi_state_consistency.rs index 165b332c..f84a3790 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency.rs @@ -30,6 +30,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { to: Some(public_mention(ctx.ctx().existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); @@ -67,6 +68,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); diff --git a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs index bce90dfb..34d5a4d7 100644 --- a/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_ffi_state_consistency_with_labels.rs @@ -46,6 +46,7 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> { to: Some(to_label.into()), to_npk: None, to_vpk: None, + to_keys: None, amount: 100, to_identifier: Some(0), }); diff --git a/integration_tests/tests/indexer_state_consistency.rs b/integration_tests/tests/indexer_state_consistency.rs index 7afbf5ae..e87927bc 100644 --- a/integration_tests/tests/indexer_state_consistency.rs +++ b/integration_tests/tests/indexer_state_consistency.rs @@ -25,6 +25,7 @@ async fn indexer_state_consistency() -> Result<()> { to: Some(public_mention(ctx.existing_public_accounts()[1])), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -60,6 +61,7 @@ async fn indexer_state_consistency() -> Result<()> { to: Some(private_mention(to)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/indexer_state_consistency_with_labels.rs b/integration_tests/tests/indexer_state_consistency_with_labels.rs index a9817933..5f561d6f 100644 --- a/integration_tests/tests/indexer_state_consistency_with_labels.rs +++ b/integration_tests/tests/indexer_state_consistency_with_labels.rs @@ -43,6 +43,7 @@ async fn indexer_state_consistency_with_labels() -> Result<()> { to: Some(CliAccountMention::Label(to_label)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); diff --git a/integration_tests/tests/keys.rs b/integration_tests/tests/keys.rs index 59628798..01af23cd 100644 --- a/integration_tests/tests/keys.rs +++ b/integration_tests/tests/keys.rs @@ -71,7 +71,10 @@ async fn sync_private_account_with_non_zero_chain_index() -> Result<()> { from: private_mention(from), to: None, to_npk: Some(hex::encode(to_account.key_chain.nullifier_public_key.0)), - to_vpk: Some(hex::encode(&to_account.key_chain.viewing_public_key.0)), + to_vpk: Some(hex::encode( + to_account.key_chain.viewing_public_key.to_bytes(), + )), + to_keys: None, to_identifier: Some(to_account.kind.identifier()), amount: 100, }); @@ -147,6 +150,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id1)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }); @@ -158,6 +162,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 101, }); @@ -197,6 +202,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id3)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 102, }); @@ -208,6 +214,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 103, }); @@ -268,6 +275,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(private_mention(to_account_id2)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 10, }); @@ -278,6 +286,7 @@ async fn restore_keys_from_seed() -> Result<()> { to: Some(public_mention(to_account_id4)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 11, }); diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index bc6de164..f96faa52 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -23,7 +23,7 @@ use lee::{ program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, + EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, account::{Account, AccountWithMetadata}, encryption::ViewingPublicKey, program::PdaSeed, @@ -64,9 +64,9 @@ async fn fund_private_pda( let sender_pre = AccountWithMetadata::new(sender_account.clone(), true, sender); let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_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 eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let instruction = Program::serialize_instruction(AuthTransferInstruction::Transfer { amount }) .context("failed to serialize auth_transfer instruction")?; @@ -74,6 +74,8 @@ async fn fund_private_pda( let account_identities = vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { + epk, + view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk), npk, ssk, identifier, @@ -89,13 +91,9 @@ async fn fund_private_pda( ) .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; - let message = Message::try_from_circuit_output( - vec![sender], - vec![sender_account.nonce], - vec![(npk, vpk, epk)], - output, - ) - .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?; + let message = + Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output) + .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?; let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -272,10 +270,10 @@ async fn private_pda_family_members_receive_and_spend() -> Result<()> { // 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_vpk_0 = ViewingPublicKey::from_seed(&[0_u8; 32], &[1_u8; 32]); let recipient_npk_1 = NullifierPublicKey([0xBB; 32]); - let recipient_vpk_1 = ViewingPublicKey::from_scalar(recipient_npk_1.0); + let recipient_vpk_1 = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]); let amount_spend_0: u128 = 13; let amount_spend_1: u128 = 37; diff --git a/integration_tests/tests/shared_accounts.rs b/integration_tests/tests/shared_accounts.rs index aa1077ff..39bdd36c 100644 --- a/integration_tests/tests/shared_accounts.rs +++ b/integration_tests/tests/shared_accounts.rs @@ -107,8 +107,11 @@ async fn group_invite_join_key_agreement() -> Result<()> { .key_chain() .sealing_secret_key() .context("Sealing key not found")?; - let sealing_pk = - key_protocol::key_management::group_key_holder::SealingPublicKey::from_scalar(sealing_sk); + let sealing_pk = key_protocol::key_management::group_key_holder::SealingPublicKey::from_bytes( + lee_core::encryption::ViewingPublicKey::from_seed(&sealing_sk.d, &sealing_sk.z) + .to_bytes() + .to_vec(), + ); let holder = ctx .wallet() @@ -204,6 +207,7 @@ async fn fund_shared_account_from_public() -> Result<()> { to: Some(private_mention(shared_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: None, amount: 100, }); diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index a5bd0ac9..b0c569e8 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -133,6 +133,7 @@ async fn create_and_transfer_public_token() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -223,6 +224,7 @@ async fn create_and_transfer_public_token() -> Result<()> { holder: Some(public_mention(recipient_account_id)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount, }; @@ -365,6 +367,7 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -554,6 +557,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(public_mention(recipient_account_id_public)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_public, }; @@ -601,6 +605,7 @@ async fn create_token_with_private_definition() -> Result<()> { holder: Some(private_mention(recipient_account_id_private)), holder_npk: None, holder_vpk: None, + holder_keys: None, holder_identifier: None, amount: mint_amount_private, }; @@ -740,6 +745,7 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -868,6 +874,7 @@ async fn shielded_token_transfer() -> Result<()> { to: Some(private_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -991,6 +998,7 @@ async fn deshielded_token_transfer() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; @@ -1124,7 +1132,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { definition: private_mention(definition_account_id), holder: None, holder_npk: Some(hex::encode(holder_keys.nullifier_public_key.0)), - holder_vpk: Some(hex::encode(&holder_keys.viewing_public_key.0)), + holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.to_bytes())), + holder_keys: None, holder_identifier: Some(holder_identifier), amount: mint_amount, }; @@ -1323,6 +1332,7 @@ async fn transfer_token_using_from_label() -> Result<()> { to: Some(public_mention(recipient_account_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: transfer_amount, }; diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index ce15a63c..459f3d61 100644 --- a/integration_tests/tests/tps.rs +++ b/integration_tests/tests/tps.rs @@ -23,7 +23,7 @@ use lee::{ public_transaction as putx, }; use lee_core::{ - InputAccountIdentity, MembershipProof, NullifierPublicKey, + EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey, account::{AccountWithMetadata, Nonce, data::Data}, encryption::ViewingPublicKey, }; @@ -256,8 +256,7 @@ pub async fn tps_test() -> Result<()> { fn build_privacy_transaction() -> PrivacyPreservingTransaction { let program = Program::authenticated_transfer_program(); let sender_nsk = [1; 32]; - let sender_vsk = [99; 32]; - let sender_vpk = ViewingPublicKey::from_scalar(sender_vsk); + let sender_vpk = ViewingPublicKey::from_seed(&[99_u8; 32], &[100_u8; 32]); let sender_npk = NullifierPublicKey::from(&sender_nsk); let sender_pre = AccountWithMetadata::new( Account { @@ -270,8 +269,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&sender_npk, 0), ); let recipient_nsk = [2; 32]; - let recipient_vsk = [99; 32]; - let recipient_vpk = ViewingPublicKey::from_scalar(recipient_vsk); + let recipient_vpk = ViewingPublicKey::from_seed(&[101_u8; 32], &[102_u8; 32]); let recipient_npk = NullifierPublicKey::from(&recipient_nsk); let recipient_pre = AccountWithMetadata::new( Account::default(), @@ -279,13 +277,13 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { AccountId::for_regular_private_account(&recipient_npk, 0), ); - let eph_holder_from = EphemeralKeyHolder::new(&sender_npk); - let sender_ss = eph_holder_from.calculate_shared_secret_sender(&sender_vpk); - let sender_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_from = EphemeralKeyHolder::new(&sender_vpk); + let sender_ss = eph_holder_from.calculate_shared_secret_sender(); + let sender_epk = eph_holder_from.ephemeral_public_key().clone(); - let eph_holder_to = EphemeralKeyHolder::new(&recipient_npk); - let recipient_ss = eph_holder_to.calculate_shared_secret_sender(&recipient_vpk); - let recipient_epk = eph_holder_from.generate_ephemeral_public_key(); + let eph_holder_to = EphemeralKeyHolder::new(&recipient_vpk); + let recipient_ss = eph_holder_to.calculate_shared_secret_sender(); + let recipient_epk = eph_holder_to.ephemeral_public_key().clone(); let balance_to_move: u128 = 1; let proof: MembershipProof = ( @@ -303,12 +301,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: sender_epk, + view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk), ssk: sender_ss, nsk: sender_nsk, membership_proof: proof, identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: recipient_epk, + view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk), npk: recipient_npk, ssk: recipient_ss, identifier: 0, @@ -317,16 +319,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction { &program.into(), ) .unwrap(); - let message = pptx::message::Message::try_from_circuit_output( - vec![], - vec![], - vec![ - (sender_npk, sender_vpk, sender_epk), - (recipient_npk, recipient_vpk, recipient_epk), - ], - output, - ) - .unwrap(); + let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]); pptx::PrivacyPreservingTransaction::new(message, witness_set) } diff --git a/integration_tests/tests/wallet_ffi.rs b/integration_tests/tests/wallet_ffi.rs index 1659288e..24f2a9c8 100644 --- a/integration_tests/tests/wallet_ffi.rs +++ b/integration_tests/tests/wallet_ffi.rs @@ -142,6 +142,7 @@ unsafe extern "C" { to_keys: *const FfiPrivateAccountKeys, to_identifier: *const FfiU128, amount: *const [u8; 16], + key_path: *const c_char, out_result: *mut FfiTransferResult, ) -> error::WalletFfiError; @@ -923,6 +924,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> { &raw const to_keys, &raw const to_identifier, &raw const amount, + std::ptr::null(), &raw mut transfer_result, ) .unwrap(); diff --git a/lee/key_protocol/Cargo.toml b/lee/key_protocol/Cargo.toml index bb8fe57d..06c244c7 100644 --- a/lee/key_protocol/Cargo.toml +++ b/lee/key_protocol/Cargo.toml @@ -19,6 +19,7 @@ common.workspace = true anyhow.workspace = true serde.workspace = true k256.workspace = true +ml-kem.workspace = true sha2.workspace = true rand.workspace = true hex.workspace = true diff --git a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs index f6e9a270..a53ae47c 100644 --- a/lee/key_protocol/src/key_management/ephemeral_key_holder.rs +++ b/lee/key_protocol/src/key_management/ephemeral_key_holder.rs @@ -1,53 +1,61 @@ use lee_core::{ - NullifierPublicKey, SharedSecretKey, - encryption::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}, + SharedSecretKey, + encryption::{EphemeralPublicKey, ViewingPublicKey}, }; -use rand::{RngCore as _, rngs::OsRng}; -use sha2::Digest as _; -#[derive(Debug)] -/// Ephemeral secret key holder. Non-clonable as intended for one-time use. Produces ephemeral -/// public keys. Can produce shared secret for sender. +/// Ephemeral key holder for the sender side of a KEM-based shared-secret exchange. +/// +/// Non-clonable as intended for one-time use: construction encapsulates once and +/// stores both the shared secret and the ciphertext (`EphemeralPublicKey`) that must +/// be sent to the receiver. pub struct EphemeralKeyHolder { - ephemeral_secret_key: EphemeralSecretKey, + shared_secret: SharedSecretKey, + ephemeral_public_key: EphemeralPublicKey, +} + +// SharedSecretKey does not implement Debug (intentional — leaking key material via +// debug output would be a security risk). We implement Debug manually here, redacting the +// shared secret while still allowing the ephemeral public key (KEM ciphertext) to be inspected. +impl std::fmt::Debug for EphemeralKeyHolder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EphemeralKeyHolder") + .field("shared_secret", &"") + .field("ephemeral_public_key", &self.ephemeral_public_key) + .finish() + } } impl EphemeralKeyHolder { #[must_use] - pub fn new(receiver_nullifier_public_key: &NullifierPublicKey) -> Self { - let mut nonce_bytes = [0; 16]; - OsRng.fill_bytes(&mut nonce_bytes); - let mut hasher = sha2::Sha256::new(); - hasher.update(receiver_nullifier_public_key); - hasher.update(nonce_bytes); - + pub fn new(receiver_viewing_public_key: &ViewingPublicKey) -> Self { + let (shared_secret, ephemeral_public_key) = + SharedSecretKey::encapsulate(receiver_viewing_public_key); Self { - ephemeral_secret_key: hasher.finalize().into(), + shared_secret, + ephemeral_public_key, } } + /// Returns the KEM ciphertext to be transmitted to the receiver as the `EphemeralPublicKey`. #[must_use] - pub fn generate_ephemeral_public_key(&self) -> EphemeralPublicKey { - EphemeralPublicKey::from_scalar(self.ephemeral_secret_key) + pub const fn ephemeral_public_key(&self) -> &EphemeralPublicKey { + &self.ephemeral_public_key } + /// Returns the sender-side shared secret (established at construction time). #[must_use] - pub fn calculate_shared_secret_sender( - &self, - receiver_viewing_public_key: &ViewingPublicKey, - ) -> SharedSecretKey { - SharedSecretKey::new(self.ephemeral_secret_key, receiver_viewing_public_key) + pub const fn calculate_shared_secret_sender(&self) -> SharedSecretKey { + self.shared_secret } } +/// Encapsulates a fresh shared secret toward `vpk` and returns `(shared_secret, ciphertext)`. +/// +/// Used when the local side is acting as an "ephemeral receiver" — i.e. generating a +/// one-sided encryption that only the holder of the VSK can decrypt. #[must_use] pub fn produce_one_sided_shared_secret_receiver( vpk: &ViewingPublicKey, ) -> (SharedSecretKey, EphemeralPublicKey) { - let mut esk = [0; 32]; - OsRng.fill_bytes(&mut esk); - ( - SharedSecretKey::new(esk, vpk), - EphemeralPublicKey::from_scalar(esk), - ) + SharedSecretKey::encapsulate(vpk) } diff --git a/lee/key_protocol/src/key_management/group_key_holder.rs b/lee/key_protocol/src/key_management/group_key_holder.rs index a989f886..7fb24713 100644 --- a/lee/key_protocol/src/key_management/group_key_holder.rs +++ b/lee/key_protocol/src/key_management/group_key_holder.rs @@ -1,44 +1,39 @@ use aes_gcm::{Aes256Gcm, KeyInit as _, aead::Aead as _}; use lee_core::{ SharedSecretKey, - encryption::{Scalar, shared_key_derivation::Secp256k1Point}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, 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}; +use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}; /// 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); +/// Wraps the ML-KEM-768 encapsulation key bytes (1184 bytes). Distinct from +/// `ViewingPublicKey` to enforce key separation: viewing keys encrypt account state, +/// sealing keys encrypt the GMS for off-chain distribution. +pub struct SealingPublicKey(Vec); 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). + /// Construct from raw serialized encapsulation-key bytes (e.g. received from another wallet). #[must_use] pub const fn from_bytes(bytes: Vec) -> Self { - Self(Secp256k1Point(bytes)) + Self(bytes) } /// Returns the raw bytes for display or transmission. #[must_use] pub fn to_bytes(&self) -> &[u8] { - &self.0.0 + &self.0 } } /// Secret key used to unseal a `GroupKeyHolder` received from another member. -pub type SealingSecretKey = Scalar; +/// Holds the two 32-byte FIPS 203 seed halves `d` and `z`. +pub type SealingSecretKey = ViewingSecretKey; /// Manages shared viewing keys for a group of controllers owning private PDAs. /// @@ -153,18 +148,17 @@ impl GroupKeyHolder { /// 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. + /// Uses ML-KEM-768 encapsulation to derive a shared secret, then AES-256-GCM to encrypt + /// the payload. The returned bytes are + /// `kem_ciphertext (1088) || nonce (12) || ciphertext+tag (48)` = 1148 bytes. /// - /// Each call generates a fresh ephemeral key, so two seals of the same holder produce + /// Each call generates a fresh KEM encapsulation, 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 sealing_key = ViewingPublicKey::from_bytes(recipient_key.0.clone()) + .expect("key_protocol::group_key_holder::GroupKeyHolder::seal_for: SealingPublicKey must be a valid ML-KEM-768 encapsulation key"); + let (shared, kem_ct) = SharedSecretKey::encapsulate(&sealing_key); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -176,12 +170,12 @@ impl GroupKeyHolder { .encrypt(&nonce, self.gms.as_ref()) .expect("AES-GCM encryption should not fail with valid key/nonce"); - let capacity = 33_usize + let capacity = 1088_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(&kem_ct.0); out.extend_from_slice(&nonce_bytes); out.extend_from_slice(&ciphertext); out @@ -189,20 +183,24 @@ impl GroupKeyHolder { /// 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; + /// Returns `Err` if the ciphertext is too short or the AES-GCM authentication tag + /// doesn't verify (wrong key or tampered data). + pub fn unseal(sealed: &[u8], own_key: &SealingSecretKey) -> Result { + // kem_ciphertext (1088) + nonce (12) = header, then AES-GCM tag (16) minimum. + const KEM_CT_LEN: usize = 1088; + const HEADER_LEN: usize = KEM_CT_LEN + 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 kem_ct = EphemeralPublicKey(sealed[..KEM_CT_LEN].to_vec()); + let nonce = aes_gcm::Nonce::from_slice(&sealed[KEM_CT_LEN..HEADER_LEN]); let ciphertext = &sealed[HEADER_LEN..]; - let shared = SharedSecretKey::new(own_key, &ephemeral_pubkey); + let shared = SharedSecretKey::decapsulate(&kem_ct, &own_key.d, &own_key.z) + .expect("key_protocol::group_key_holder::GroupKeyHolder::unseal: KEM_CT_LEN guarantees exactly 1088 bytes"); let aes_key = Self::seal_kdf(&shared); let cipher = Aes256Gcm::new(&aes_key.into()); @@ -219,7 +217,7 @@ impl GroupKeyHolder { Ok(Self::from_gms(gms)) } - /// Derives an AES-256 key from the ECDH shared secret via SHA-256 with a domain prefix. + /// Derives an AES-256 key from the ML-KEM 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(); @@ -407,8 +405,10 @@ mod tests { 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"); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + let restored = GroupKeyHolder::unseal(&sealed, &recipient_vsk).expect("unseal"); assert_eq!(restored.dangerous_raw_gms(), holder.dangerous_raw_gms()); @@ -433,13 +433,14 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let wrong_ssk = SecretSpendingKey([99_u8; 32]); - let wrong_vsk = wrong_ssk + let wrong_vsk = SecretSpendingKey([99_u8; 32]) .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); + let sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + let result = GroupKeyHolder::unseal(&sealed, &wrong_vsk); assert!(matches!(result, Err(super::SealError::DecryptionFailed))); } @@ -453,16 +454,18 @@ mod tests { 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 mut sealed = holder.seal_for(&SealingPublicKey::from_bytes( + recipient_vpk.to_bytes().to_vec(), + )); + // Flip a byte in the AES-GCM ciphertext portion (after KEM ciphertext + nonce). let last = sealed.len() - 1; sealed[last] ^= 0xFF; - let result = GroupKeyHolder::unseal(&sealed, recipient_vsk); + 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). + /// Two seals of the same holder produce different ciphertexts (KEM randomness). #[test] fn two_seals_produce_different_ciphertexts() { let holder = GroupKeyHolder::from_gms([42_u8; 32]); @@ -472,7 +475,7 @@ mod tests { .produce_private_key_holder(None) .generate_viewing_public_key(); - let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.0); + let sealing_key = SealingPublicKey::from_bytes(recipient_vpk.to_bytes().to_vec()); let sealed_a = holder.seal_for(&sealing_key); let sealed_b = holder.seal_for(&sealing_key); assert_ne!(sealed_a, sealed_b); @@ -481,14 +484,15 @@ mod tests { /// 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); + let vsk = SealingSecretKey { + d: [7_u8; 32], + z: [0_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. + /// Degenerate GMS values must still produce valid, non-zero, pairwise-distinct npks. #[test] fn degenerate_gms_produces_distinct_non_zero_keys() { let seed = PdaSeed::new([1; 32]); @@ -526,21 +530,19 @@ mod tests { let pda_seed = PdaSeed::new([42_u8; 32]); let program_id: lee_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 sealed = + alice_holder.seal_for(&SealingPublicKey::from_bytes(bob_vpk.to_bytes().to_vec())); let bob_holder = - GroupKeyHolder::unseal(&sealed, bob_vsk).expect("Bob should unseal the GMS"); + 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(); diff --git a/lee/key_protocol/src/key_management/key_tree/keys_private.rs b/lee/key_protocol/src/key_management/key_tree/keys_private.rs index cd86720b..5a27be79 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_private.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_private.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; -use k256::{Scalar, elliptic_curve::PrimeField as _}; use lee_core::{NullifierPublicKey, PrivateAccountKind, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; +use sha2::Digest as _; use crate::key_management::{ KeyChain, @@ -34,10 +34,10 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(None); - let vsk = ssk.generate_viewing_secret_key(None); + let vsk = ssk.generate_viewing_secret_seed_key(None); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from_scalar(vsk); + let vpk = ViewingPublicKey::from(&vsk); Self { value: ( @@ -59,16 +59,20 @@ impl ChildKeysPrivate { #[must_use] pub fn nth_child(&self, cci: u32) -> Self { - #[expect(clippy::arithmetic_side_effects, reason = "TODO: fix later")] - let parent_pt = - Scalar::from_repr(self.value.0.private_key_holder.nullifier_secret_key.into()) - .expect("Key generated as scalar, must be valid representation") - * Scalar::from_repr(self.value.0.private_key_holder.viewing_secret_key.into()) - .expect("Key generated as scalar, must be valid representation"); - let mut input = vec![]; + // `parent_hash`` is used to incorporate entropy based on the parent node's keys + // to generate the `ssk` and `ccc` values. + let mut parent_hash = sha2::Sha256::new(); + parent_hash.update(b"LEE/keys"); + parent_hash.update(self.value.0.private_key_holder.nullifier_secret_key); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.d); + parent_hash.update(self.value.0.private_key_holder.viewing_secret_key.z); + let parent_pt = parent_hash.finalize(); + // Each child (of the same parent node) share the same `parent_pt`. + // To ensure that each child generates unique keys, we include the child index. + let mut input = vec![]; input.extend_from_slice(b"LEE_seed_priv"); - input.extend_from_slice(&parent_pt.to_bytes()); + input.extend_from_slice(&parent_pt); #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] input.extend_from_slice(&cci.to_be_bytes()); @@ -84,10 +88,10 @@ impl ChildKeysPrivate { .expect("hash_value is 64 bytes, must be safe to get last 32"); let nsk = ssk.generate_nullifier_secret_key(Some(cci)); - let vsk = ssk.generate_viewing_secret_key(Some(cci)); + let vsk = ssk.generate_viewing_secret_seed_key(Some(cci)); let npk = NullifierPublicKey::from(&nsk); - let vpk = ViewingPublicKey::from_scalar(vsk); + let vpk = ViewingPublicKey::from(&vsk); Self { value: ( @@ -128,12 +132,11 @@ impl KeyTreeNode for ChildKeysPrivate { #[cfg(test)] mod tests { - use lee_core::{NullifierPublicKey, NullifierSecretKey}; + use lee_core::NullifierSecretKey; use super::*; use crate::key_management::{self, secret_holders::ViewingSecretKey}; - #[expect(clippy::redundant_type_annotations, reason = "TODO: clippy requires")] #[test] fn master_key_generation() { let seed: [u8; 64] = [ @@ -145,7 +148,7 @@ mod tests { let keys = ChildKeysPrivate::root(seed); - let expected_ssk: SecretSpendingKey = key_management::secret_holders::SecretSpendingKey([ + let expected_ssk = key_management::secret_holders::SecretSpendingKey([ 246, 79, 26, 124, 135, 95, 52, 51, 201, 27, 48, 194, 2, 144, 51, 219, 245, 128, 139, 222, 42, 195, 105, 33, 115, 97, 186, 0, 97, 14, 218, 191, ]); @@ -160,26 +163,92 @@ mod tests { 34, 234, 19, 222, 2, 22, 12, 163, 252, 88, 11, 0, 163, ]; - let expected_npk: NullifierPublicKey = lee_core::NullifierPublicKey([ + let expected_npk = lee_core::NullifierPublicKey([ 7, 123, 125, 191, 233, 183, 201, 4, 20, 214, 155, 210, 45, 234, 27, 240, 194, 111, 97, 247, 155, 113, 122, 246, 192, 0, 70, 61, 76, 71, 70, 2, ]); - let expected_vsk = [ - 155, 90, 54, 75, 228, 130, 68, 201, 129, 251, 180, 195, 250, 64, 34, 230, 241, 204, - 216, 50, 149, 156, 10, 67, 208, 74, 9, 10, 47, 59, 50, 202, - ]; - let expected_vpk_as_bytes: [u8; 33] = [ - 2, 191, 99, 102, 114, 40, 131, 109, 166, 8, 222, 186, 107, 29, 156, 106, 206, 96, 127, - 80, 170, 66, 217, 79, 38, 80, 11, 74, 147, 123, 221, 159, 166, - ]; + let expected_vsk = ViewingSecretKey::new( + [ + 187, 143, 146, 12, 68, 148, 25, 203, 21, 92, 131, 2, 221, 81, 117, 62, 98, 194, + 159, 177, 102, 254, 236, 182, 76, 242, 116, 219, 17, 166, 99, 36, + ], + [ + 80, 97, 83, 209, 145, 99, 168, 99, 89, 29, 153, 236, 82, 99, 134, 114, 168, 19, + 223, 69, 34, 47, 76, 76, 15, 97, 245, 184, 25, 103, 251, 82, + ], + ); + let expected_vpk: [u8; 1184] = [ + 127, 229, 162, 212, 104, 117, 4, 150, 192, 103, 122, 195, 14, 35, 12, 60, 52, 23, 220, + 150, 100, 203, 34, 34, 127, 232, 156, 43, 218, 109, 6, 160, 67, 35, 210, 194, 25, 181, + 118, 237, 25, 129, 51, 160, 189, 51, 99, 184, 57, 28, 121, 240, 236, 2, 170, 198, 26, + 91, 172, 110, 52, 32, 186, 35, 179, 202, 234, 249, 15, 242, 100, 198, 168, 163, 120, + 205, 118, 85, 195, 210, 187, 95, 150, 154, 8, 68, 165, 237, 87, 166, 101, 57, 4, 18, + 11, 122, 235, 180, 199, 154, 165, 158, 55, 136, 30, 237, 43, 167, 215, 68, 80, 102, 0, + 71, 90, 130, 206, 240, 215, 69, 199, 83, 7, 60, 184, 128, 230, 184, 61, 93, 201, 204, + 165, 104, 9, 127, 220, 52, 246, 217, 131, 251, 2, 170, 133, 6, 51, 40, 224, 101, 61, + 16, 135, 32, 182, 201, 68, 58, 171, 54, 161, 184, 243, 38, 106, 200, 251, 17, 172, 8, + 24, 73, 230, 55, 85, 20, 147, 222, 165, 200, 116, 135, 47, 20, 227, 56, 220, 64, 120, + 215, 245, 58, 86, 102, 149, 252, 193, 163, 160, 59, 82, 138, 249, 171, 1, 54, 199, 193, + 171, 85, 38, 64, 56, 121, 106, 84, 57, 252, 94, 147, 16, 191, 196, 104, 47, 129, 84, + 21, 252, 160, 81, 207, 184, 199, 3, 177, 74, 117, 115, 175, 138, 108, 36, 198, 5, 32, + 15, 218, 3, 20, 19, 15, 251, 209, 86, 128, 139, 148, 78, 10, 34, 144, 149, 74, 102, 48, + 59, 70, 124, 47, 193, 100, 26, 9, 104, 178, 102, 156, 199, 242, 101, 147, 161, 87, 27, + 234, 192, 204, 41, 36, 43, 83, 219, 15, 211, 66, 91, 76, 73, 13, 113, 155, 203, 193, + 160, 130, 84, 103, 47, 70, 100, 147, 169, 65, 119, 84, 121, 122, 161, 76, 203, 144, + 248, 145, 22, 8, 46, 121, 44, 77, 20, 149, 66, 179, 56, 149, 231, 98, 184, 9, 64, 14, + 67, 196, 34, 8, 123, 21, 80, 169, 168, 223, 230, 133, 0, 66, 159, 230, 69, 201, 205, + 169, 105, 196, 21, 71, 84, 70, 58, 165, 165, 134, 186, 232, 60, 70, 51, 57, 239, 74, + 174, 116, 234, 36, 178, 49, 42, 168, 250, 104, 141, 106, 0, 109, 52, 86, 104, 243, 62, + 214, 137, 48, 107, 2, 152, 206, 227, 175, 147, 236, 19, 113, 27, 191, 231, 235, 167, + 114, 104, 23, 126, 203, 94, 242, 149, 171, 115, 170, 89, 244, 58, 29, 176, 73, 203, 44, + 8, 32, 9, 226, 32, 78, 246, 38, 235, 149, 133, 25, 243, 47, 124, 180, 200, 211, 165, + 137, 56, 169, 117, 31, 244, 65, 91, 135, 146, 158, 20, 75, 102, 32, 65, 250, 103, 199, + 36, 48, 31, 155, 164, 191, 222, 85, 37, 66, 243, 17, 120, 104, 0, 228, 83, 200, 116, 6, + 199, 106, 236, 139, 246, 216, 152, 241, 211, 85, 106, 200, 44, 231, 240, 66, 3, 193, + 147, 16, 145, 65, 49, 33, 53, 247, 69, 47, 44, 113, 86, 117, 6, 20, 193, 183, 128, 178, + 181, 21, 251, 99, 39, 149, 210, 146, 106, 181, 186, 7, 36, 63, 186, 234, 191, 164, 193, + 162, 127, 250, 122, 189, 219, 21, 92, 48, 86, 209, 184, 99, 160, 201, 162, 145, 20, + 138, 154, 18, 37, 180, 209, 165, 165, 51, 187, 78, 193, 175, 135, 6, 55, 216, 178, 10, + 40, 246, 98, 128, 80, 14, 38, 69, 113, 123, 54, 94, 43, 50, 106, 167, 17, 77, 163, 148, + 117, 225, 9, 7, 253, 240, 157, 96, 103, 33, 100, 37, 37, 20, 53, 138, 234, 55, 45, 232, + 154, 9, 150, 192, 116, 36, 119, 106, 95, 119, 34, 220, 84, 174, 19, 227, 33, 209, 96, + 197, 148, 230, 197, 59, 117, 130, 7, 116, 11, 0, 197, 16, 249, 151, 31, 4, 64, 29, 165, + 247, 110, 176, 166, 4, 112, 136, 101, 208, 7, 179, 38, 183, 134, 58, 107, 207, 160, 38, + 159, 67, 112, 20, 225, 199, 179, 133, 117, 144, 54, 199, 15, 204, 80, 154, 116, 84, 88, + 109, 113, 5, 207, 226, 21, 62, 247, 122, 14, 156, 9, 8, 76, 26, 148, 67, 196, 128, 176, + 78, 51, 161, 151, 75, 248, 154, 31, 168, 9, 4, 3, 107, 222, 245, 178, 21, 84, 7, 25, + 155, 118, 97, 135, 63, 89, 233, 11, 207, 148, 155, 38, 106, 104, 102, 140, 104, 67, + 149, 20, 30, 196, 44, 197, 128, 34, 182, 80, 30, 32, 137, 34, 212, 164, 177, 164, 12, + 115, 41, 156, 111, 71, 230, 120, 111, 218, 25, 117, 218, 75, 167, 32, 37, 57, 50, 99, + 181, 203, 40, 105, 248, 150, 114, 121, 73, 127, 198, 191, 161, 44, 56, 213, 243, 71, 2, + 56, 192, 243, 107, 179, 27, 96, 21, 116, 169, 64, 15, 97, 166, 151, 200, 11, 40, 204, + 71, 168, 220, 9, 55, 43, 146, 244, 212, 166, 192, 180, 189, 237, 162, 42, 29, 33, 52, + 193, 4, 178, 157, 244, 28, 209, 44, 26, 36, 147, 126, 94, 164, 37, 47, 115, 38, 23, + 165, 96, 106, 140, 42, 69, 146, 194, 93, 71, 175, 49, 147, 32, 246, 97, 94, 41, 116, + 127, 174, 18, 16, 14, 163, 17, 180, 213, 203, 166, 33, 139, 214, 18, 170, 27, 41, 59, + 175, 200, 101, 14, 128, 45, 179, 167, 136, 232, 138, 56, 124, 145, 75, 233, 132, 161, + 196, 164, 72, 80, 60, 187, 38, 90, 90, 17, 66, 134, 59, 2, 165, 29, 76, 24, 38, 211, + 177, 83, 119, 20, 239, 59, 77, 34, 3, 42, 47, 60, 89, 46, 103, 168, 120, 17, 199, 50, + 17, 103, 107, 48, 8, 53, 220, 159, 212, 65, 198, 80, 8, 11, 235, 97, 203, 196, 240, 44, + 56, 121, 77, 91, 196, 160, 129, 242, 149, 226, 57, 106, 180, 76, 161, 203, 18, 37, 166, + 153, 44, 40, 28, 74, 8, 11, 6, 166, 54, 10, 103, 247, 23, 35, 7, 47, 173, 133, 71, 85, + 3, 168, 250, 120, 126, 174, 37, 80, 128, 107, 7, 161, 130, 155, 136, 92, 48, 215, 119, + 196, 124, 85, 157, 234, 2, 166, 137, 65, 121, 222, 112, 47, 17, 43, 23, 111, 88, 5, + 195, 41, 8, 191, 227, 21, 173, 35, 199, 196, 188, 162, 191, 195, 204, 137, 54, 16, 73, + 178, 150, 249, 234, 22, 216, 123, 157, 144, 218, 118, 53, 193, 67, 65, 84, 162, 244, + 165, 24, 110, 246, 146, 228, 212, 180, 150, 116, 201, 37, 128, 76, 41, 188, 42, 79, + 148, 52, 196, 176, 178, 224, 48, 168, 13, 129, 193, 131, 185, 131, 93, 40, 145, 56, + 180, 29, 153, 83, 39, 69, 232, 96, 238, 137, 104, 150, 2, 202, 239, 149, 248, 154, 115, + 115, 127, 3, 8, 32, 61, 96, 66, 25, 181, 14, 72, 73, 97, 186, 134, 140, 33, 69, 33, 74, + ]; assert!(expected_ssk == keys.value.0.secret_spending_key); assert!(expected_ccc == keys.ccc); assert!(expected_nsk == keys.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == keys.value.0.nullifier_public_key); assert!(expected_vsk == keys.value.0.private_key_holder.viewing_secret_key); - assert!(expected_vpk_as_bytes == keys.value.0.viewing_public_key.to_bytes()); + assert!(expected_vpk == keys.value.0.viewing_public_key.to_bytes()); } #[test] @@ -194,33 +263,107 @@ mod tests { let root_node = ChildKeysPrivate::root(seed); let child_node = ChildKeysPrivate::nth_child(&root_node, 42_u32); - let expected_ccc: [u8; 32] = [ - 27, 73, 133, 213, 214, 63, 217, 184, 164, 17, 172, 140, 223, 95, 255, 157, 11, 0, 58, - 53, 82, 147, 121, 120, 199, 50, 30, 28, 103, 24, 121, 187, + let expected_ssk = key_management::secret_holders::SecretSpendingKey([ + 151, 183, 113, 151, 215, 187, 207, 64, 197, 182, 207, 32, 5, 49, 180, 98, 119, 14, 248, + 175, 39, 100, 47, 109, 148, 173, 217, 253, 159, 234, 209, 113, + ]); + + let expected_ccc = [ + 138, 243, 142, 163, 62, 107, 63, 131, 230, 158, 185, 60, 204, 50, 243, 222, 13, 123, + 98, 116, 131, 194, 7, 25, 129, 209, 163, 72, 178, 143, 192, 240, ]; let expected_nsk: NullifierSecretKey = [ - 124, 61, 40, 92, 33, 135, 3, 41, 200, 234, 3, 69, 102, 184, 57, 191, 106, 151, 194, - 192, 103, 132, 141, 112, 249, 108, 192, 117, 24, 48, 70, 216, + 196, 33, 11, 39, 220, 84, 119, 182, 187, 194, 135, 20, 124, 33, 244, 205, 96, 58, 102, + 52, 74, 67, 110, 213, 24, 16, 160, 64, 247, 3, 107, 235, ]; let expected_npk = lee_core::NullifierPublicKey([ - 116, 231, 246, 189, 145, 240, 37, 59, 219, 223, 216, 246, 116, 171, 223, 55, 197, 200, - 134, 192, 221, 40, 218, 167, 239, 5, 11, 95, 147, 247, 162, 226, + 247, 253, 217, 86, 157, 208, 39, 172, 59, 190, 88, 165, 7, 173, 183, 106, 172, 211, 4, + 180, 51, 107, 177, 107, 51, 117, 231, 176, 200, 103, 1, 121, ]); - let expected_vsk: ViewingSecretKey = [ - 33, 155, 68, 60, 102, 70, 47, 105, 194, 129, 44, 26, 143, 198, 44, 244, 185, 31, 236, - 252, 205, 89, 138, 107, 39, 38, 154, 73, 109, 166, 41, 114, - ]; - let expected_vpk_as_bytes: [u8; 33] = [ - 2, 78, 213, 113, 117, 105, 162, 248, 175, 68, 128, 232, 106, 204, 208, 159, 11, 78, 48, - 244, 127, 112, 46, 0, 93, 184, 1, 77, 132, 160, 75, 152, 88, + let expected_vsk = ViewingSecretKey::new( + [ + 185, 209, 179, 92, 7, 131, 98, 121, 215, 46, 154, 56, 238, 106, 162, 225, 83, 82, + 134, 3, 80, 186, 35, 178, 161, 204, 205, 163, 28, 19, 149, 18, + ], + [ + 174, 24, 72, 205, 129, 123, 131, 9, 146, 152, 224, 151, 10, 184, 224, 109, 94, 149, + 117, 60, 26, 10, 212, 125, 113, 147, 87, 67, 73, 26, 101, 193, + ], + ); + + let expected_vpk: [u8; 1184] = [ + 215, 229, 207, 120, 148, 177, 148, 197, 72, 222, 134, 3, 231, 146, 123, 226, 36, 84, + 232, 179, 205, 16, 241, 142, 9, 81, 58, 54, 12, 115, 148, 182, 19, 245, 22, 203, 57, + 71, 11, 204, 156, 130, 30, 170, 199, 201, 25, 2, 21, 34, 155, 136, 124, 145, 223, 128, + 177, 207, 92, 38, 252, 165, 118, 61, 128, 71, 154, 242, 105, 165, 52, 7, 6, 244, 120, + 227, 134, 191, 25, 169, 150, 123, 246, 138, 25, 196, 126, 156, 144, 33, 123, 120, 44, + 142, 89, 201, 49, 219, 205, 87, 236, 110, 64, 129, 102, 100, 155, 26, 101, 121, 42, + 236, 82, 111, 141, 117, 75, 71, 194, 73, 123, 170, 110, 69, 149, 107, 96, 195, 55, 122, + 140, 131, 106, 140, 156, 147, 75, 28, 128, 138, 113, 86, 37, 63, 173, 214, 200, 2, 214, + 84, 234, 176, 120, 252, 184, 99, 192, 65, 112, 150, 99, 26, 174, 187, 183, 187, 64, 90, + 248, 100, 66, 63, 195, 3, 44, 43, 128, 59, 149, 107, 66, 180, 67, 200, 183, 200, 36, + 91, 7, 65, 228, 159, 79, 44, 89, 35, 163, 145, 92, 227, 104, 2, 72, 5, 7, 193, 21, 51, + 116, 198, 184, 6, 192, 188, 68, 183, 163, 193, 142, 244, 217, 155, 197, 187, 189, 174, + 225, 45, 126, 112, 93, 194, 156, 102, 150, 1, 188, 222, 76, 108, 73, 149, 44, 28, 219, + 66, 95, 215, 204, 148, 217, 16, 36, 121, 112, 2, 51, 10, 195, 137, 12, 93, 203, 146, + 138, 211, 15, 201, 42, 72, 146, 186, 160, 222, 235, 127, 83, 48, 182, 49, 248, 29, 138, + 16, 32, 232, 179, 163, 187, 161, 174, 152, 187, 93, 76, 166, 48, 230, 219, 111, 123, + 181, 103, 130, 28, 109, 235, 115, 45, 57, 193, 206, 160, 17, 52, 92, 194, 25, 3, 80, + 97, 142, 249, 151, 94, 250, 95, 12, 57, 11, 165, 92, 47, 85, 182, 48, 22, 60, 97, 244, + 59, 194, 135, 180, 133, 106, 227, 56, 192, 60, 91, 15, 241, 146, 89, 240, 130, 219, + 202, 187, 43, 85, 98, 50, 104, 64, 114, 113, 80, 54, 69, 69, 5, 43, 90, 19, 0, 0, 188, + 251, 184, 70, 160, 18, 117, 76, 53, 209, 166, 96, 34, 224, 137, 115, 183, 168, 243, 19, + 1, 255, 4, 97, 162, 199, 104, 72, 213, 111, 62, 54, 172, 82, 184, 82, 143, 71, 99, 25, + 104, 74, 120, 70, 84, 235, 32, 22, 20, 218, 163, 77, 194, 125, 75, 22, 72, 236, 192, + 200, 107, 91, 156, 201, 10, 178, 87, 19, 181, 211, 91, 17, 145, 200, 17, 179, 65, 75, + 200, 186, 89, 144, 91, 184, 116, 214, 51, 91, 42, 162, 243, 202, 92, 18, 54, 0, 213, + 67, 149, 151, 51, 29, 220, 196, 160, 201, 68, 113, 210, 164, 175, 152, 121, 168, 231, + 161, 91, 132, 218, 1, 171, 176, 84, 100, 57, 1, 3, 2, 196, 194, 76, 181, 79, 171, 157, + 35, 162, 155, 192, 210, 149, 142, 120, 189, 127, 151, 96, 202, 225, 73, 242, 81, 112, + 237, 224, 155, 130, 130, 34, 196, 153, 131, 161, 113, 163, 172, 114, 48, 207, 32, 151, + 172, 83, 145, 79, 210, 100, 161, 92, 82, 216, 90, 104, 238, 212, 38, 50, 107, 17, 228, + 195, 190, 6, 151, 165, 148, 245, 102, 51, 8, 185, 8, 85, 59, 247, 219, 95, 219, 170, + 155, 233, 123, 27, 64, 251, 56, 24, 200, 16, 181, 212, 146, 61, 116, 106, 215, 214, 62, + 118, 27, 68, 233, 148, 73, 135, 199, 74, 184, 89, 159, 217, 139, 24, 208, 250, 30, 224, + 97, 185, 237, 193, 8, 216, 23, 186, 5, 50, 41, 161, 203, 22, 217, 23, 194, 191, 148, + 124, 10, 212, 171, 209, 210, 145, 184, 171, 74, 35, 220, 43, 145, 241, 23, 43, 92, 171, + 216, 43, 114, 77, 155, 147, 156, 86, 56, 170, 27, 1, 54, 182, 169, 96, 22, 201, 51, + 145, 94, 143, 133, 106, 47, 176, 112, 197, 197, 96, 80, 73, 164, 207, 179, 22, 229, + 171, 201, 223, 219, 13, 219, 1, 91, 224, 252, 171, 199, 217, 25, 60, 128, 135, 9, 71, + 105, 231, 86, 34, 21, 155, 50, 0, 105, 72, 117, 108, 175, 140, 9, 181, 249, 139, 97, 3, + 161, 66, 248, 42, 67, 113, 132, 8, 119, 232, 6, 169, 18, 157, 222, 53, 176, 56, 137, + 120, 18, 115, 199, 187, 112, 48, 223, 211, 206, 152, 252, 108, 179, 129, 20, 227, 248, + 183, 234, 87, 202, 49, 17, 69, 215, 118, 89, 188, 180, 33, 238, 245, 206, 40, 179, 129, + 242, 59, 73, 254, 117, 114, 250, 179, 103, 109, 250, 202, 99, 152, 2, 167, 130, 169, + 35, 71, 89, 211, 140, 71, 103, 154, 121, 108, 147, 191, 186, 73, 10, 73, 203, 23, 55, + 106, 144, 98, 227, 157, 25, 27, 81, 67, 11, 57, 88, 227, 116, 61, 100, 94, 23, 166, + 146, 57, 226, 72, 124, 33, 65, 226, 35, 167, 206, 156, 202, 213, 213, 158, 89, 249, + 181, 19, 113, 109, 217, 71, 168, 142, 180, 122, 30, 5, 54, 170, 155, 73, 56, 170, 124, + 139, 4, 165, 103, 82, 32, 183, 84, 7, 239, 117, 135, 239, 48, 24, 28, 210, 49, 137, 6, + 158, 65, 211, 113, 205, 135, 146, 83, 10, 46, 90, 27, 97, 135, 135, 185, 173, 69, 58, + 34, 247, 141, 150, 6, 158, 117, 23, 198, 139, 65, 81, 179, 187, 194, 247, 203, 127, + 106, 232, 119, 122, 215, 197, 110, 69, 203, 174, 227, 63, 185, 106, 14, 184, 104, 113, + 233, 83, 92, 104, 38, 188, 9, 135, 107, 108, 121, 193, 33, 209, 89, 39, 137, 17, 208, + 26, 21, 238, 169, 86, 181, 193, 153, 82, 8, 151, 53, 39, 88, 91, 252, 3, 33, 75, 127, + 9, 168, 53, 34, 1, 173, 202, 123, 157, 174, 170, 199, 254, 187, 196, 144, 37, 29, 48, + 112, 173, 107, 147, 155, 69, 134, 137, 156, 247, 123, 242, 72, 5, 43, 106, 89, 179, + 204, 41, 15, 60, 48, 78, 214, 180, 26, 170, 67, 71, 66, 146, 113, 220, 159, 153, 201, + 176, 116, 154, 21, 186, 33, 180, 72, 39, 187, 240, 80, 112, 132, 144, 173, 210, 12, 76, + 184, 146, 89, 178, 178, 82, 109, 71, 201, 241, 160, 207, 219, 124, 77, 2, 105, 124, + 178, 71, 3, 38, 64, 41, 83, 170, 137, 82, 242, 144, 76, 102, 82, 7, 25, 149, 141, 169, + 46, 4, 68, 40, 244, 146, 131, 107, 148, 18, 111, 85, 104, 243, 28, 75, 176, 249, 88, + 82, 123, 89, 29, 104, 135, 230, 117, 67, 26, 249, 108, 145, 76, 38, 175, 89, 185, 94, + 106, 128, 201, 150, 151, 194, 133, 21, 81, 213, 231, 15, 117, 44, 61, 86, 223, 162, 56, + 190, 166, 177, 157, 137, 60, 208, 155, 234, 158, 252, 30, ]; + assert!(expected_ssk == child_node.value.0.secret_spending_key); assert!(expected_ccc == child_node.ccc); assert!(expected_nsk == child_node.value.0.private_key_holder.nullifier_secret_key); assert!(expected_npk == child_node.value.0.nullifier_public_key); assert!(expected_vsk == child_node.value.0.private_key_holder.viewing_secret_key); - assert!(expected_vpk_as_bytes == child_node.value.0.viewing_public_key.to_bytes()); + assert!(expected_vpk == child_node.value.0.viewing_public_key.to_bytes()); } } diff --git a/lee/key_protocol/src/key_management/key_tree/keys_public.rs b/lee/key_protocol/src/key_management/key_tree/keys_public.rs index a8f97070..947fb83c 100644 --- a/lee/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/lee/key_protocol/src/key_management/key_tree/keys_public.rs @@ -1,4 +1,4 @@ -use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; +use k256::elliptic_curve::PrimeField as _; use serde::{Deserialize, Serialize}; use crate::key_management::key_tree::traits::KeyTreeNode; @@ -6,9 +6,13 @@ use crate::key_management::key_tree::traits::KeyTreeNode; #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] pub struct ChildKeysPublic { - pub csk: lee::PrivateKey, - pub cpk: lee::PublicKey, - pub ccc: [u8; 32], + /// Secret key for public account. + pub sk: lee::PrivateKey, + /// Schnorr secret key. + pub ssk: lee::PrivateKey, + /// Schnorr public key. + pub pk: lee::PublicKey, + pub cc: [u8; 32], /// Can be [`None`] if root. pub cci: Option, } @@ -18,19 +22,24 @@ impl ChildKeysPublic { pub fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); - let csk = lee::PrivateKey::try_new( + let sk = lee::PrivateKey::try_new( *hash_value .first_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get first 32"), ) .expect("Expect a valid Private Key"); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = lee::PublicKey::new_from_private_key(&csk); + let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`"); + + let cc = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); + let pk = lee::PublicKey::new_from_private_key(&ssk); Self { - csk, - cpk, - ccc, + sk, + ssk, + pk, + cc, cci: None, } } @@ -39,61 +48,53 @@ impl ChildKeysPublic { pub fn nth_child(&self, cci: u32) -> Self { let hash_value = self.compute_hash_value(cci); - let csk = lee::PrivateKey::try_new({ - let hash_value = hash_value + let lhs = k256::Scalar::from_repr( + (*hash_value .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"); + .expect("hash_value is 64 bytes, must be safe to get first 32")) + .into(), + ) + .expect("Expect a valid k256 scalar"); + let rhs = + k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar"); - let value_1 = - k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar"); - let value_2 = k256::Scalar::from_repr((*self.csk.value()).into()) - .expect("Expect a valid k256 scalar"); + let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into()) + .expect("Expect a valid private key"); - let sum = value_1.add(&value_2); - sum.to_bytes().into() - }) - .expect("Expect a valid private key"); + let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`"); - let ccc = *hash_value + let cc = *hash_value .last_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get last 32"); - let cpk = lee::PublicKey::new_from_private_key(&csk); + let pk = lee::PublicKey::new_from_private_key(&ssk); Self { - csk, - cpk, - ccc, + sk, + ssk, + pk, + cc, cci: Some(cci), } } #[must_use] pub fn account_id(&self) -> lee::AccountId { - lee::AccountId::from(&self.cpk) + lee::AccountId::from(&self.pk) } fn compute_hash_value(&self, cci: u32) -> [u8; 64] { let mut hash_input = vec![]; - - if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater { - // Non-harden. - // BIP-032 compatibility requires 1-byte header from the public_key; - // Not stored in `self.cpk.value()`. - let sk = k256::SecretKey::from_bytes(self.csk.value().into()) - .expect("32 bytes, within curve order"); - let pk = sk.public_key(); - hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes()); - } else { - // Harden. - hash_input.extend_from_slice(&[0_u8]); - hash_input.extend_from_slice(self.csk.value()); - } + // Simplified key logic by only supporting harden keys. + // Non-harden keys would require access to untweaked public keys associated to `sk`s. + // Thus, not PQ secure. + hash_input.extend_from_slice(&[0_u8]); + hash_input.extend_from_slice(self.sk.value()); #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] hash_input.extend_from_slice(&cci.to_be_bytes()); - hmac_sha512::HMAC::mac(hash_input, self.ccc) + hmac_sha512::HMAC::mac(hash_input, self.cc) } } @@ -103,7 +104,7 @@ impl ChildKeysPublic { )] impl<'a> From<&'a ChildKeysPublic> for &'a lee::PrivateKey { fn from(value: &'a ChildKeysPublic) -> Self { - &value.csk + &value.ssk } } @@ -137,30 +138,37 @@ mod tests { ]; let keys = ChildKeysPublic::root(seed); - let expected_ccc = [ + let expected_cc = [ 238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222, 13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155, ]; - let expected_csk: PrivateKey = PrivateKey::try_new([ + let expected_sk: PrivateKey = PrivateKey::try_new([ 40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36, 202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247, - 244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245, + let expected_ssk: PrivateKey = PrivateKey::try_new([ + 207, 4, 246, 223, 104, 72, 19, 85, 14, 122, 194, 82, 32, 163, 60, 57, 8, 25, 209, 91, + 254, 107, 76, 238, 31, 68, 236, 192, 154, 78, 105, 118, ]) .unwrap(); - assert!(expected_ccc == keys.ccc); - assert!(expected_csk == keys.csk); - assert!(expected_cpk == keys.cpk); + let expected_pk: PublicKey = PublicKey::try_new([ + 188, 163, 203, 45, 151, 154, 230, 254, 123, 114, 158, 130, 19, 182, 164, 143, 150, 131, + 176, 7, 27, 58, 204, 116, 5, 247, 0, 255, 111, 160, 52, 201, + ]) + .unwrap(); + + assert!(expected_cc == keys.cc); + assert!(expected_ssk == keys.ssk); + assert!(expected_sk == keys.sk); + assert!(expected_pk == keys.pk); } #[test] - fn harden_child_keys_generation() { + fn child_keys_generation() { let seed = [ 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, @@ -171,93 +179,32 @@ mod tests { let cci = (2_u32).pow(31) + 13; let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - let expected_ccc = [ + let expected_cc = [ 149, 226, 13, 4, 194, 12, 69, 29, 9, 234, 209, 119, 98, 4, 128, 91, 37, 103, 192, 31, 130, 126, 123, 20, 90, 34, 173, 209, 101, 248, 155, 36, ]; - let expected_csk: PrivateKey = PrivateKey::try_new([ + let expected_sk: PrivateKey = PrivateKey::try_new([ 9, 65, 33, 228, 25, 82, 219, 117, 91, 217, 11, 223, 144, 85, 246, 26, 123, 216, 107, 213, 33, 52, 188, 22, 198, 246, 71, 46, 245, 174, 16, 47, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 142, 143, 238, 159, 105, 165, 224, 252, 108, 62, 53, 209, 176, 219, 249, 38, 90, 241, - 201, 81, 194, 146, 236, 5, 83, 152, 238, 243, 138, 16, 229, 15, + let expected_ssk: PrivateKey = PrivateKey::try_new([ + 100, 37, 212, 81, 40, 233, 72, 156, 177, 139, 50, 114, 136, 157, 202, 132, 203, 246, + 252, 242, 13, 81, 42, 100, 159, 240, 187, 252, 202, 108, 25, 105, ]) .unwrap(); - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); - } - - #[test] - fn nonharden_child_keys_generation() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let root_keys = ChildKeysPublic::root(seed); - let cci = 13; - let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - - let expected_ccc = [ - 79, 228, 242, 119, 211, 203, 198, 175, 95, 36, 4, 234, 139, 45, 137, 138, 54, 211, 187, - 16, 28, 79, 80, 232, 216, 101, 145, 19, 101, 220, 217, 141, - ]; - - let expected_csk: PrivateKey = PrivateKey::try_new([ - 185, 147, 32, 242, 145, 91, 123, 77, 42, 33, 134, 84, 12, 165, 117, 70, 158, 201, 95, - 153, 14, 12, 92, 235, 128, 156, 194, 169, 68, 35, 165, 127, + let expected_pk: PublicKey = PublicKey::try_new([ + 210, 59, 119, 137, 21, 153, 82, 22, 195, 82, 12, 16, 80, 156, 125, 199, 19, 173, 46, + 224, 213, 144, 165, 126, 70, 129, 171, 141, 77, 212, 108, 233, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 119, 16, 145, 121, 97, 244, 186, 35, 136, 34, 140, 171, 206, 139, 11, 208, 207, 121, - 158, 45, 28, 22, 140, 98, 161, 179, 212, 173, 238, 220, 2, 34, - ]) - .unwrap(); - - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); - } - - #[test] - fn edge_case_child_keys_generation_2_power_31() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let root_keys = ChildKeysPublic::root(seed); - let cci = (2_u32).pow(31); //equivant to 0, thus non-harden. - let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - - let expected_ccc = [ - 221, 208, 47, 189, 174, 152, 33, 25, 151, 114, 233, 191, 57, 15, 40, 140, 46, 87, 126, - 58, 215, 40, 246, 111, 166, 113, 183, 145, 173, 11, 27, 182, - ]; - - let expected_csk: PrivateKey = PrivateKey::try_new([ - 223, 29, 87, 189, 126, 24, 117, 225, 190, 57, 0, 143, 207, 168, 231, 139, 170, 192, 81, - 254, 126, 10, 115, 42, 141, 157, 70, 171, 199, 231, 198, 132, - ]) - .unwrap(); - - let expected_cpk: PublicKey = PublicKey::try_new([ - 96, 123, 245, 51, 214, 216, 215, 205, 70, 145, 105, 221, 166, 169, 122, 27, 94, 112, - 228, 110, 249, 177, 85, 173, 180, 248, 185, 199, 112, 246, 83, 33, - ]) - .unwrap(); - - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); + assert!(expected_cc == child_keys.cc); + assert!(expected_ssk == child_keys.ssk); + assert!(expected_sk == child_keys.sk); + assert!(expected_pk == child_keys.pk); } } diff --git a/lee/key_protocol/src/key_management/key_tree/mod.rs b/lee/key_protocol/src/key_management/key_tree/mod.rs index 3d93427b..c15c09a5 100644 --- a/lee/key_protocol/src/key_management/key_tree/mod.rs +++ b/lee/key_protocol/src/key_management/key_tree/mod.rs @@ -347,8 +347,8 @@ mod tests { assert!(tree.key_map.contains_key(&ChainIndex::root())); assert!(tree.account_id_map.contains_key(&AccountId::new([ - 172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178, - 38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247 + 10, 231, 159, 65, 236, 46, 205, 5, 172, 89, 250, 29, 123, 195, 212, 137, 155, 111, 40, + 120, 53, 28, 124, 54, 224, 170, 119, 208, 2, 72, 75, 50 ]))); } diff --git a/lee/key_protocol/src/key_management/mod.rs b/lee/key_protocol/src/key_management/mod.rs index 61d4b40d..459badf0 100644 --- a/lee/key_protocol/src/key_management/mod.rs +++ b/lee/key_protocol/src/key_management/mod.rs @@ -69,21 +69,15 @@ impl KeyChain { pub fn calculate_shared_secret_receiver( &self, ephemeral_public_key_sender: &EphemeralPublicKey, - index: Option, - ) -> SharedSecretKey { - SharedSecretKey::new( - self.secret_spending_key.generate_viewing_secret_key(index), - ephemeral_public_key_sender, - ) + ) -> Option { + let vsk = &self.private_key_holder.viewing_secret_key; + SharedSecretKey::decapsulate(ephemeral_public_key_sender, &vsk.d, &vsk.z) } } #[cfg(test)] mod tests { - use aes_gcm::aead::OsRng; use base58::ToBase58 as _; - use k256::{AffinePoint, elliptic_curve::group::GroupEncoding as _}; - use rand::RngCore as _; use super::*; use crate::key_management::{ @@ -106,14 +100,31 @@ mod tests { fn calculate_shared_secret_receiver() { let account_id_key_holder = KeyChain::new_os_random(); - // Generate a random ephemeral public key sender - let mut scalar = [0; 32]; - OsRng.fill_bytes(&mut scalar); - let ephemeral_public_key_sender = EphemeralPublicKey::from_scalar(scalar); + // Create a proper KEM ciphertext by encapsulating toward this key chain's VPK. + let (_, epk) = SharedSecretKey::encapsulate(&account_id_key_holder.viewing_public_key); - // Calculate shared secret - let _shared_secret = account_id_key_holder - .calculate_shared_secret_receiver(&ephemeral_public_key_sender, None); + let _shared_secret = account_id_key_holder.calculate_shared_secret_receiver(&epk); + } + + #[test] + fn calculate_shared_secret_receiver_returns_none_for_malformed_epk() { + let key_chain = KeyChain::new_os_random(); + + let short_epk = EphemeralPublicKey(vec![42_u8; 100]); + assert!( + key_chain + .calculate_shared_secret_receiver(&short_epk) + .is_none(), + "short EphemeralPublicKey must return None" + ); + + let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + assert!( + key_chain + .calculate_shared_secret_receiver(&long_epk) + .is_none(), + "long EphemeralPublicKey must return None" + ); } #[test] @@ -135,12 +146,6 @@ mod tests { println!("======Prerequisites======"); println!(); - println!( - "Group generator {:?}", - hex::encode(AffinePoint::GENERATOR.to_bytes()) - ); - println!(); - println!("======Holders======"); println!(); @@ -188,14 +193,12 @@ mod tests { fn non_trivial_chain_index() { let keys = account_with_chain_index_2_for_tests(); - let eph_key_holder = EphemeralKeyHolder::new(&keys.nullifier_public_key); + let eph_key_holder = EphemeralKeyHolder::new(&keys.viewing_public_key); - let key_sender = eph_key_holder.calculate_shared_secret_sender(&keys.viewing_public_key); - let key_receiver = keys.calculate_shared_secret_receiver( - &eph_key_holder.generate_ephemeral_public_key(), - Some(2), - ); + let key_sender = eph_key_holder.calculate_shared_secret_sender(); + let key_receiver = + keys.calculate_shared_secret_receiver(eph_key_holder.ephemeral_public_key()); - assert_eq!(key_sender.0, key_receiver.0); + assert_eq!(key_sender.0, key_receiver.unwrap().0); } } diff --git a/lee/key_protocol/src/key_management/secret_holders.rs b/lee/key_protocol/src/key_management/secret_holders.rs index 50e5657b..7bda4ffb 100644 --- a/lee/key_protocol/src/key_management/secret_holders.rs +++ b/lee/key_protocol/src/key_management/secret_holders.rs @@ -1,9 +1,7 @@ use bip39::Mnemonic; use common::HashType; -use lee_core::{ - NullifierPublicKey, NullifierSecretKey, - encryption::{Scalar, ViewingPublicKey}, -}; +use lee_core::{NullifierPublicKey, NullifierSecretKey, encryption::ViewingPublicKey}; +use ml_kem; use rand::{RngCore as _, rngs::OsRng}; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, digest::FixedOutput as _}; @@ -19,8 +17,20 @@ pub struct SeedHolder { /// Secret spending key object. Can produce `PrivateKeyHolder` objects. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct SecretSpendingKey(pub [u8; 32]); +/// Viewing secret key: the FIPS 203 KEM seed split into its two 32-byte halves `d` and `z`, +/// from which the ML-KEM-768 decapsulation key is derived deterministically. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ViewingSecretKey { + pub d: [u8; 32], + pub z: [u8; 32], +} -pub type ViewingSecretKey = Scalar; +impl ViewingSecretKey { + #[must_use] + pub const fn new(d: [u8; 32], z: [u8; 32]) -> Self { + Self { d, z } + } +} /// Private key holder. Produces public keys. Can produce `account_id`. Can produce shared secret /// for recepient. @@ -114,7 +124,7 @@ impl SecretSpendingKey { #[must_use] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] - pub fn generate_viewing_secret_key(&self, index: Option) -> ViewingSecretKey { + pub fn generate_viewing_secret_seed_key(&self, index: Option) -> ViewingSecretKey { const PREFIX: &[u8; 8] = b"LEE/keys"; const SUFFIX_1: &[u8; 1] = &[2]; const SUFFIX_2: &[u8; 19] = &[0; 19]; @@ -124,25 +134,57 @@ impl SecretSpendingKey { _ => index.expect("Expect a valid u32"), }; - let mut hasher = sha2::Sha256::new(); - hasher.update(PREFIX); - hasher.update(self.0); - hasher.update(SUFFIX_1); - hasher.update(index.to_be_bytes()); - hasher.update(SUFFIX_2); + let mut bytes: Vec = Vec::with_capacity(64); + bytes.extend_from_slice(PREFIX); + bytes.extend_from_slice(&self.0); + bytes.extend_from_slice(SUFFIX_1); + bytes.extend_from_slice(&index.to_be_bytes()); + bytes.extend_from_slice(SUFFIX_2); + let bytes: [u8; 64] = bytes + .try_into() + .expect("`generate_viewing_secret_seed_key`: bytes must be exactly 64"); - hasher.finalize_fixed().into() + let full_seed = hmac_sha512::HMAC::mac(bytes, b"LEE_viewing_seed"); + + ViewingSecretKey::new( + *full_seed + .first_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get first 32"), + *full_seed + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"), + ) + } + + #[must_use] + pub const fn generate_viewing_secret_key(seed: [u8; 64]) -> ViewingSecretKey { + ViewingSecretKey::new( + *seed.first_chunk::<32>().expect("seed is 64 bytes"), + *seed.last_chunk::<32>().expect("seed is 64 bytes"), + ) } #[must_use] pub fn produce_private_key_holder(&self, index: Option) -> PrivateKeyHolder { PrivateKeyHolder { nullifier_secret_key: self.generate_nullifier_secret_key(index), - viewing_secret_key: self.generate_viewing_secret_key(index), + viewing_secret_key: self.generate_viewing_secret_seed_key(index), } } } +impl From<&ViewingSecretKey> for ViewingPublicKey { + fn from(sk: &ViewingSecretKey) -> Self { + use ml_kem::{Kem, KeyExport as _, MlKem768, Seed}; + let mut seed_bytes = [0_u8; 64]; + seed_bytes[..32].copy_from_slice(&sk.d); + seed_bytes[32..].copy_from_slice(&sk.z); + let dk = ::DecapsulationKey::from_seed(Seed::from(seed_bytes)); + Self::from_bytes(dk.encapsulation_key().to_bytes().to_vec()) + .expect("key_protocol::secret_holders::From<&ViewingSecretKey>: ML-KEM-768 encapsulation key is always 1184 bytes") + } +} + impl PrivateKeyHolder { #[must_use] pub fn generate_nullifier_public_key(&self) -> NullifierPublicKey { @@ -151,7 +193,7 @@ impl PrivateKeyHolder { #[must_use] pub fn generate_viewing_public_key(&self) -> ViewingPublicKey { - ViewingPublicKey::from_scalar(self.viewing_secret_key) + ViewingPublicKey::from(&self.viewing_secret_key) } } @@ -183,8 +225,7 @@ mod tests { assert_eq!(seed_holder.seed.len(), 64); let top_secret_key_holder = seed_holder.produce_top_secret_key_holder(); - - let _vsk = top_secret_key_holder.generate_viewing_secret_key(None); + let _vsk = top_secret_key_holder.generate_viewing_secret_seed_key(None); } #[test] diff --git a/lee/state_machine/Cargo.toml b/lee/state_machine/Cargo.toml index 194c402f..8777db09 100644 --- a/lee/state_machine/Cargo.toml +++ b/lee/state_machine/Cargo.toml @@ -31,6 +31,7 @@ risc0-build = "3.0.3" risc0-binfmt = "3.0.2" [dev-dependencies] +lee_core = { workspace = true, features = ["test_utils"] } token_core.workspace = true authenticated_transfer_core.workspace = true test_program_methods.workspace = true diff --git a/lee/state_machine/core/Cargo.toml b/lee/state_machine/core/Cargo.toml index 4447d859..6e1f0ff0 100644 --- a/lee/state_machine/core/Cargo.toml +++ b/lee/state_machine/core/Cargo.toml @@ -16,7 +16,7 @@ thiserror.workspace = true bytemuck.workspace = true bytesize.workspace = true base58.workspace = true -k256 = { workspace = true, optional = true } +ml-kem = { workspace = true, optional = true, features = ["getrandom"] } chacha20 = { version = "0.10" } [dev-dependencies] @@ -24,4 +24,5 @@ serde_json.workspace = true [features] default = [] -host = ["dep:k256"] +host = ["dep:ml-kem"] +test_utils = ["host"] diff --git a/lee/state_machine/core/src/circuit_io.rs b/lee/state_machine/core/src/circuit_io.rs index b1c2e44f..78bfa24f 100644 --- a/lee/state_machine/core/src/circuit_io.rs +++ b/lee/state_machine/core/src/circuit_io.rs @@ -4,7 +4,7 @@ use crate::{ Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{Account, AccountWithMetadata}, - encryption::Ciphertext, + encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag}, program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, }; @@ -33,6 +33,8 @@ pub enum InputAccountIdentity { /// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and /// matched against `pre_state.account_id`. PrivateAuthorizedInit { + epk: EphemeralPublicKey, + view_tag: ViewTag, ssk: SharedSecretKey, nsk: NullifierSecretKey, identifier: Identifier, @@ -40,6 +42,8 @@ pub enum InputAccountIdentity { /// Update of an authorized standalone private account: existing on-chain commitment, with /// membership proof. PrivateAuthorizedUpdate { + epk: EphemeralPublicKey, + view_tag: ViewTag, ssk: SharedSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, @@ -48,6 +52,8 @@ pub enum InputAccountIdentity { /// 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 { + epk: EphemeralPublicKey, + view_tag: ViewTag, npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, @@ -57,6 +63,8 @@ pub enum InputAccountIdentity { /// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it /// as the 4th input. PrivatePdaInit { + epk: EphemeralPublicKey, + view_tag: ViewTag, npk: NullifierPublicKey, ssk: SharedSecretKey, identifier: Identifier, @@ -72,6 +80,8 @@ pub enum InputAccountIdentity { /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a /// previously-seen authorization in a chained call. PrivatePdaUpdate { + epk: EphemeralPublicKey, + view_tag: ViewTag, ssk: SharedSecretKey, nsk: NullifierSecretKey, membership_proof: MembershipProof, @@ -123,7 +133,7 @@ impl InputAccountIdentity { pub struct PrivacyPreservingCircuitOutput { pub public_pre_states: Vec, pub public_post_states: Vec, - pub ciphertexts: Vec, + pub encrypted_private_post_states: Vec, pub new_commitments: Vec, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, pub block_validity_window: BlockValidityWindow, @@ -148,6 +158,7 @@ mod tests { use crate::{ Commitment, Nullifier, account::{Account, AccountId, AccountWithMetadata, Nonce}, + encryption::Ciphertext, }; #[test] @@ -181,7 +192,11 @@ mod tests { data: b"post state data".to_vec().try_into().unwrap(), nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF), }], - ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]), + epk: EphemeralPublicKey(vec![9, 9, 9]), + view_tag: 42, + }], new_commitments: vec![Commitment::new( &AccountId::new([1; 32]), &Account::default(), diff --git a/lee/state_machine/core/src/encoding.rs b/lee/state_machine/core/src/encoding.rs index f4b081bf..59df4b06 100644 --- a/lee/state_machine/core/src/encoding.rs +++ b/lee/state_machine/core/src/encoding.rs @@ -7,7 +7,7 @@ use std::io::Read as _; #[cfg(feature = "host")] use crate::Nullifier; #[cfg(feature = "host")] -use crate::encryption::shared_key_derivation::Secp256k1Point; +use crate::encryption::EphemeralPublicKey; #[cfg(feature = "host")] use crate::error::LeeCoreError; use crate::{ @@ -158,16 +158,17 @@ impl Ciphertext { } #[cfg(feature = "host")] -impl Secp256k1Point { - /// Converts the point to bytes. +impl EphemeralPublicKey { + /// Serializes the ML-KEM-768 ciphertext to bytes (always 1088 bytes). #[must_use] - pub fn to_bytes(&self) -> [u8; 33] { - self.0.clone().try_into().unwrap() + pub fn to_bytes(&self) -> Vec { + self.0.clone() } - /// Deserializes a secp256k1 point from a cursor. + /// Deserializes an ML-KEM-768 ciphertext from a cursor. + /// Reads exactly 1088 bytes — the fixed ciphertext size for ML-KEM-768. pub fn from_cursor(cursor: &mut Cursor<&[u8]>) -> Result { - let mut value = vec![0; 33]; + let mut value = vec![0_u8; 1088]; cursor.read_exact(&mut value)?; Ok(Self(value)) } diff --git a/lee/state_machine/core/src/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index f97f9021..5fa80b60 100644 --- a/lee/state_machine/core/src/encryption/mod.rs +++ b/lee/state_machine/core/src/encryption/mod.rs @@ -6,7 +6,7 @@ use chacha20::{ use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde::{Deserialize, Serialize}; #[cfg(feature = "host")] -pub use shared_key_derivation::{EphemeralPublicKey, EphemeralSecretKey, ViewingPublicKey}; +pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey}; use crate::{Commitment, account::Account, program::PrivateAccountKind}; #[cfg(feature = "host")] @@ -17,6 +17,11 @@ pub type Scalar = [u8; 32]; #[derive(Serialize, Deserialize, Clone, Copy)] pub struct SharedSecretKey(pub [u8; 32]); +/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the +/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct EphemeralPublicKey(pub Vec); + pub struct EncryptionScheme; #[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] @@ -36,6 +41,45 @@ impl std::fmt::Debug for Ciphertext { } } +pub type ViewTag = u8; + +/// Encrypted private-account note for one output. +#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] +#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))] +pub struct EncryptedAccountData { + pub ciphertext: Ciphertext, + pub epk: EphemeralPublicKey, + pub view_tag: ViewTag, +} + +#[cfg(feature = "host")] +impl EncryptedAccountData { + #[must_use] + pub fn new( + ciphertext: Ciphertext, + npk: &crate::NullifierPublicKey, + vpk: &ViewingPublicKey, + epk: EphemeralPublicKey, + ) -> Self { + let view_tag = Self::compute_view_tag(npk, vpk); + Self { + ciphertext, + epk, + view_tag, + } + } + + /// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk). + #[must_use] + pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag { + let mut bytes = Vec::new(); + bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/"); + bytes.extend_from_slice(&npk.to_byte_array()); + bytes.extend_from_slice(vpk.to_bytes()); + Impl::hash_bytes(&bytes).as_bytes()[0] + } +} + impl EncryptionScheme { #[must_use] pub fn encrypt( @@ -154,4 +198,41 @@ mod tests { assert_eq!(account_ct.0.len(), pda_ct.0.len()); } + + /// Verifies the full account-note pipeline: ML-KEM-768 encapsulation/decapsulation + /// feeds the correct shared secret into the SHA-256 KDF and `ChaCha20` round-trip. + #[cfg(feature = "host")] + #[test] + fn kem_to_chacha20_round_trip() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + let vpk = shared_key_derivation::ViewingPublicKey::from_seed(&d, &z); + + let (sender_ss, epk) = SharedSecretKey::encapsulate(&vpk); + let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); + + let account = Account { + program_owner: [12_u32; 8], + balance: 999, + ..Account::default() + }; + let kind = PrivateAccountKind::Regular(0); + let commitment = crate::Commitment::new(&AccountId::new([7_u8; 32]), &account); + + let ct = EncryptionScheme::encrypt(&account, &kind, &sender_ss, &commitment, 0); + let (decoded_kind, decoded_account) = + EncryptionScheme::decrypt(&ct, &receiver_ss, &commitment, 0) + .expect("decryption must succeed with correct shared secret"); + + assert_eq!(decoded_account, account); + assert_eq!(decoded_kind, kind); + + // Wrong shared secret must not decrypt correctly. + let wrong_ss = SharedSecretKey([0_u8; 32]); + let bad = EncryptionScheme::decrypt(&ct, &wrong_ss, &commitment, 0); + assert!( + bad.is_none() || bad.is_some_and(|(_, a)| a.balance != 999), + "wrong shared secret must not produce the correct plaintext" + ); + } } diff --git a/lee/state_machine/core/src/encryption/shared_key_derivation.rs b/lee/state_machine/core/src/encryption/shared_key_derivation.rs index 8ea5aac8..5c982c6f 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -1,78 +1,227 @@ -#![expect( - clippy::arithmetic_side_effects, - reason = "Multiplication of finite field elements can't overflow" -)] - -use std::fmt::Write as _; - use borsh::{BorshDeserialize, BorshSerialize}; -use k256::{ - AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, - elliptic_curve::{ - PrimeField as _, - sec1::{FromEncodedPoint as _, ToEncodedPoint as _}, - }, -}; +use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; use serde::{Deserialize, Serialize}; -use crate::{SharedSecretKey, encryption::Scalar}; +use crate::{EphemeralPublicKey, SharedSecretKey}; +/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate). #[derive( - Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, BorshSerialize, BorshDeserialize, + Serialize, + Deserialize, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, )] -pub struct Secp256k1Point(pub Vec); +pub struct MlKem768EncapsulationKey(Vec); -impl std::fmt::Debug for Secp256k1Point { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let hex: String = self.0.iter().fold(String::new(), |mut acc, b| { - write!(acc, "{b:02x}").expect("writing to string should not fail"); - acc - }); - write!(f, "Secp256k1Point({hex})") +pub type ViewingPublicKey = MlKem768EncapsulationKey; + +impl MlKem768EncapsulationKey { + /// Expected byte length of an ML-KEM-768 encapsulation key. + pub const LEN: usize = 1184; + + /// Construct from raw bytes, returning an error if the length is not [`Self::LEN`]. + pub fn from_bytes(bytes: Vec) -> Result { + if bytes.len() != Self::LEN { + return Err(crate::error::LeeCoreError::DeserializationError(format!( + "MlKem768EncapsulationKey must be {} bytes, got {}", + Self::LEN, + bytes.len() + ))); + } + Ok(Self(bytes)) } -} -impl Secp256k1Point { #[must_use] - pub fn from_scalar(value: Scalar) -> Self { - let x_bytes: FieldBytes = value.into(); - let x = k256::Scalar::from_repr(x_bytes).unwrap(); - - let p = ProjectivePoint::GENERATOR * x; - let q = AffinePoint::from(p); - let enc = q.to_encoded_point(true); - - Self(enc.as_bytes().to_vec()) + pub fn to_bytes(&self) -> &[u8] { + &self.0 } -} -pub type EphemeralSecretKey = Scalar; -pub type EphemeralPublicKey = Secp256k1Point; -pub type ViewingPublicKey = Secp256k1Point; -impl From<&EphemeralSecretKey> for EphemeralPublicKey { - fn from(value: &EphemeralSecretKey) -> Self { - Self::from_scalar(*value) + /// Derive the ML-KEM-768 encapsulation key from the FIPS 203 seed halves `d` and `z`. + #[must_use] + pub fn from_seed(d: &[u8; 32], z: &[u8; 32]) -> Self { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(z); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + Self(dk.encapsulation_key().to_bytes().to_vec()) } } impl SharedSecretKey { - /// Creates a new shared secret key from a scalar and a point. + /// Sender: encapsulate a fresh shared secret toward `ek`. + /// + /// Returns `(shared_secret, ciphertext)`. The ciphertext must be included in the transaction + /// as the `EphemeralPublicKey`; the receiver recovers the same shared secret via + /// [`Self::decapsulate`]. #[must_use] - pub fn new(scalar: Scalar, point: &Secp256k1Point) -> Self { - let scalar = k256::Scalar::from_repr(scalar.into()).unwrap(); - let point: [u8; 33] = point.0.clone().try_into().unwrap(); + pub fn encapsulate(ek: &MlKem768EncapsulationKey) -> (Self, EphemeralPublicKey) { + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate(); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - let encoded = EncodedPoint::from_bytes(point).unwrap(); - let pubkey_affine = AffinePoint::from_encoded_point(&encoded).unwrap(); + /// Deterministically encapsulate a shared secret toward `ek` for use in tests. + /// + /// The shared secret has no secret entropy — it is fully determined by `ek`, + /// `message_hash`, and `output_index`, all of which are public. This makes it + /// unsuitable for real encryption but useful for producing stable, reproducible + /// shared secrets in unit tests. Use a distinct `output_index` per output to + /// avoid EPK collisions across multiple outputs in the same test. + /// + /// For production use [`Self::encapsulate`], which draws randomness from the OS. + #[cfg(any(test, feature = "test_utils"))] + #[must_use] + pub fn encapsulate_deterministic( + ek: &MlKem768EncapsulationKey, + message_hash: &[u8; 32], + output_index: u32, + ) -> (Self, EphemeralPublicKey) { + use risc0_zkvm::sha::{Impl, Sha256 as _}; - let shared = ProjectivePoint::from(pubkey_affine) * scalar; - let shared_affine = shared.to_affine(); + let mut input = Vec::with_capacity(36); + input.extend_from_slice(message_hash); + input.extend_from_slice(&output_index.to_le_bytes()); + let hash = Impl::hash_bytes(&input); + let m: ml_kem::B32 = + ml_kem::array::Array::try_from(hash.as_bytes()).expect("SHA-256 output is 32 bytes"); - let shared_affine_encoded = shared_affine.to_encoded_point(false); - let x_bytes_slice = shared_affine_encoded.x().unwrap(); - let mut x_bytes = [0_u8; 32]; - x_bytes.copy_from_slice(x_bytes_slice); + let ek_bytes: ml_kem::kem::Key = + ek.0.as_slice() + .try_into() + .expect("MlKem768EncapsulationKey must be 1184 bytes"); + let ek_obj = ml_kem::EncapsulationKey768::new(&ek_bytes).expect( + "MlKem768EncapsulationKey bytes must encode a valid ML-KEM-768 encapsulation key", + ); + let (ct, ss) = ek_obj.encapsulate_deterministic(&m); + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + (Self(ss_bytes), EphemeralPublicKey(ct.to_vec())) + } - Self(x_bytes) + /// Receiver: decapsulate the shared secret from a KEM ciphertext. + /// + /// Returns `None` if the `EphemeralPublicKey` is not exactly 1088 bytes — callers on + /// the wallet scan path should skip the output rather than panic on malformed chain data. + /// + /// `d` and `z` are the two 32-byte halves of the FIPS 203 `ViewingSecretKey` seed. + #[must_use] + pub fn decapsulate( + ciphertext: &EphemeralPublicKey, + d: &[u8; 32], + z: &[u8; 32], + ) -> Option { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(d); + seed[32..].copy_from_slice(z); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ss = dk.decapsulate_slice(&ciphertext.0).ok()?; + let ss_bytes: [u8; 32] = ss + .as_slice() + .try_into() + .expect("ML-KEM shared key is 32 bytes"); + Some(Self(ss_bytes)) + } +} + +#[cfg(test)] +mod tests { + use ml_kem::KeyExport as _; + + use super::*; + + #[test] + fn encapsulate_decapsulate_round_trip() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d); + seed[32..].copy_from_slice(&z); + + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + let ek_bytes = dk.encapsulation_key().to_bytes(); + let ek = MlKem768EncapsulationKey(ek_bytes.to_vec()); + + let (sender_ss, epk) = SharedSecretKey::encapsulate(&ek); + let receiver_ss = SharedSecretKey::decapsulate(&epk, &d, &z).unwrap(); + + assert_eq!(sender_ss.0, receiver_ss.0, "shared secrets must match"); + assert_eq!(epk.0.len(), 1088, "ML-KEM-768 ciphertext is 1088 bytes"); + assert_eq!( + ek.0.len(), + 1184, + "ML-KEM-768 encapsulation key is 1184 bytes" + ); + } + + #[test] + fn decapsulate_returns_none_for_malformed_epk() { + let d = [1_u8; 32]; + let z = [2_u8; 32]; + + // Too short — 100 bytes instead of 1088. + let short_epk = EphemeralPublicKey(vec![42_u8; 100]); + assert!( + SharedSecretKey::decapsulate(&short_epk, &d, &z).is_none(), + "short EphemeralPublicKey must return None" + ); + + // Too long — 1089 bytes instead of 1088. + let long_epk = EphemeralPublicKey(vec![42_u8; 1089]); + assert!( + SharedSecretKey::decapsulate(&long_epk, &d, &z).is_none(), + "long EphemeralPublicKey must return None" + ); + + // Empty. + let empty_epk = EphemeralPublicKey(vec![]); + assert!( + SharedSecretKey::decapsulate(&empty_epk, &d, &z).is_none(), + "empty EphemeralPublicKey must return None" + ); + } + + #[test] + fn different_vpks_produce_different_shared_secrets() { + let (d1, z1) = ([1_u8; 32], [2_u8; 32]); + let (d2, z2) = ([3_u8; 32], [4_u8; 32]); + + let ek1 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d1); + seed[32..].copy_from_slice(&z1); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + let ek2 = { + let mut seed = Seed::default(); + seed[..32].copy_from_slice(&d2); + seed[32..].copy_from_slice(&z2); + let dk = ml_kem::DecapsulationKey768::from_seed(seed); + MlKem768EncapsulationKey(dk.encapsulation_key().to_bytes().to_vec()) + }; + + let (ss1, _) = SharedSecretKey::encapsulate(&ek1); + let (ss2, _) = SharedSecretKey::encapsulate(&ek2); + + assert_ne!(ss1.0, ss2.0); } } diff --git a/lee/state_machine/core/src/lib.rs b/lee/state_machine/core/src/lib.rs index 466e1f5d..9ad2858e 100644 --- a/lee/state_machine/core/src/lib.rs +++ b/lee/state_machine/core/src/lib.rs @@ -10,7 +10,9 @@ pub use commitment::{ Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, compute_digest_for_path, }; -pub use encryption::{EncryptionScheme, SharedSecretKey}; +pub use encryption::{ + EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, SharedSecretKey, ViewTag, +}; pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey}; pub use program::PrivateAccountKind; diff --git a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs index fee77581..87c7c5bb 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -31,7 +31,9 @@ impl Proof { } pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool { - let inner: InnerReceipt = borsh::from_slice(&self.0).unwrap(); + let Ok(inner) = borsh::from_slice::(&self.0) else { + return false; + }; let receipt = Receipt::new(inner, circuit_output.to_bytes()); receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok() } @@ -176,8 +178,8 @@ mod tests { #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] use lee_core::{ - Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, - PrivacyPreservingCircuitOutput, SharedSecretKey, + Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, + EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, program::{PdaSeed, PrivateAccountKind}, }; @@ -199,7 +201,7 @@ mod tests { idx: usize, ) -> PrivateAccountKind { let (kind, _) = EncryptionScheme::decrypt( - &output.ciphertexts[idx], + &output.encrypted_private_post_states[idx].ciphertext, ssk, &output.new_commitments[idx], u32::try_from(idx).expect("idx fits in u32"), @@ -208,6 +210,17 @@ mod tests { kind } + #[test] + fn proof_inner_roundtrip() { + // `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches + // mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`, + // and of `from_inner` discarding its argument. + let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF]; + assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes); + assert!(Proof::from_inner(vec![]).into_inner().is_empty()); + assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]); + } + #[test] fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { let recipient_keys = test_private_account_keys_1(); @@ -243,8 +256,8 @@ mod tests { let expected_sender_pre = sender.clone(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; let (output, proof) = execute_and_prove( vec![sender, recipient], @@ -255,6 +268,11 @@ mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), ssk: shared_secret, identifier: 0, @@ -272,10 +290,10 @@ mod tests { assert_eq!(sender_post, expected_sender_post); assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_nullifiers.len(), 1); - assert_eq!(output.ciphertexts.len(), 1); + assert_eq!(output.encrypted_private_post_states.len(), 1); let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.ciphertexts[0], + &output.encrypted_private_post_states[0].ciphertext, &shared_secret, &output.new_commitments[0], 0, @@ -340,11 +358,11 @@ mod tests { Commitment::new(&recipient_account_id, &expected_private_account_2), ]; - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); + let shared_secret_1 = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; - let esk_2 = [5; 32]; - let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); + let shared_secret_2 = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0; let (output, proof) = execute_and_prove( vec![sender_pre, recipient], @@ -354,6 +372,11 @@ mod tests { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret_1, nsk: sender_keys.nsk, membership_proof: commitment_set @@ -362,6 +385,11 @@ mod tests { identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, @@ -376,10 +404,10 @@ mod tests { assert!(output.public_post_states.is_empty()); assert_eq!(output.new_commitments, expected_new_commitments); assert_eq!(output.new_nullifiers, expected_new_nullifiers); - assert_eq!(output.ciphertexts.len(), 2); + assert_eq!(output.encrypted_private_post_states.len(), 2); let (_identifier, sender_post) = EncryptionScheme::decrypt( - &output.ciphertexts[0], + &output.encrypted_private_post_states[0].ciphertext, &shared_secret_1, &expected_new_commitments[0], 0, @@ -388,7 +416,7 @@ mod tests { assert_eq!(sender_post, expected_private_account_1); let (_identifier, recipient_post) = EncryptionScheme::decrypt( - &output.ciphertexts[1], + &output.encrypted_private_post_states[1].ciphertext, &shared_secret_2, &expected_new_commitments[1], 1, @@ -418,8 +446,8 @@ mod tests { )) .unwrap(); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0; let program_with_deps = ProgramWithDependencies::new( validity_window_chain_caller, @@ -430,6 +458,11 @@ mod tests { vec![pre], instruction, vec![InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &account_keys.npk(), + &account_keys.vpk(), + ), npk: account_keys.npk(), ssk: shared_secret, identifier: 0, @@ -449,7 +482,8 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -458,6 +492,8 @@ mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier, @@ -487,7 +523,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -504,6 +541,8 @@ mod tests { vec![pda_pre], instruction, vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret_pda, identifier: 0, @@ -526,7 +565,8 @@ mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret_pda = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret_pda = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; // PDA (new, private PDA) let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0); @@ -556,6 +596,8 @@ mod tests { instruction, vec![ InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret_pda, identifier: 0, @@ -581,7 +623,8 @@ mod tests { 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()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0; // Sender: public account with balance, owned by auth-transfer let sender_id = AccountId::new([99; 32]); @@ -612,6 +655,11 @@ mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &shared_npk, + &shared_keys.vpk(), + ), npk: shared_npk, ssk: shared_secret, identifier: shared_identifier, @@ -632,7 +680,7 @@ mod tests { 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 ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let pre = AccountWithMetadata::new(Account::default(), true, account_id); @@ -641,6 +689,8 @@ mod tests { Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) .unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), ssk, nsk: keys.nsk, identifier, @@ -662,7 +712,7 @@ mod tests { 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 ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let sender = AccountWithMetadata::new( Account { @@ -685,6 +735,8 @@ mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), npk: keys.npk(), ssk, identifier, @@ -707,7 +759,7 @@ mod tests { 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 ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier); let account = Account { program_owner: program.id(), @@ -729,6 +781,8 @@ mod tests { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()), ssk, nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), @@ -756,7 +810,7 @@ mod tests { let npk = keys.npk(); let seed = PdaSeed::new([42; 32]); let identifier: u128 = 99; - let ssk = SharedSecretKey::new([55; 32], &keys.vpk()); + let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier); @@ -783,6 +837,8 @@ mod tests { Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), ssk, nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), @@ -811,7 +867,8 @@ mod tests { 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 shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -820,6 +877,8 @@ mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: 99, @@ -838,7 +897,7 @@ mod tests { 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 ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let auth_transfer_id = auth_transfer.id(); let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5); @@ -863,6 +922,8 @@ mod tests { Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), ssk, nsk: keys.nsk, membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), diff --git a/lee/state_machine/src/privacy_preserving_transaction/message.rs b/lee/state_machine/src/privacy_preserving_transaction/message.rs index 9065ccc4..b2594912 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/message.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/message.rs @@ -1,52 +1,16 @@ use borsh::{BorshDeserialize, BorshSerialize}; use lee_core::{ - Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, + Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, Nonce}, - encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey}, program::{BlockValidityWindow, TimestampValidityWindow}, }; +pub use lee_core::{EncryptedAccountData, ViewTag}; use sha2::{Digest as _, Sha256}; use crate::{AccountId, error::LeeError}; const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00"; -pub type ViewTag = u8; - -#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub struct EncryptedAccountData { - pub ciphertext: Ciphertext, - pub epk: EphemeralPublicKey, - pub view_tag: ViewTag, -} - -impl EncryptedAccountData { - fn new( - ciphertext: Ciphertext, - npk: &NullifierPublicKey, - vpk: &ViewingPublicKey, - epk: EphemeralPublicKey, - ) -> Self { - let view_tag = Self::compute_view_tag(npk, vpk); - Self { - ciphertext, - epk, - view_tag, - } - } - - /// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || Npk || vpk). - #[must_use] - pub fn compute_view_tag(npk: &NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag { - let mut hasher = Sha256::new(); - hasher.update(b"/LEE/v0.3/ViewTag/"); - hasher.update(npk.to_byte_array()); - hasher.update(vpk.to_bytes()); - let digest: [u8; 32] = hasher.finalize().into(); - digest[0] - } -} - #[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct Message { pub public_account_ids: Vec, @@ -92,28 +56,13 @@ impl Message { pub fn try_from_circuit_output( public_account_ids: Vec, nonces: Vec, - public_keys: Vec<(NullifierPublicKey, ViewingPublicKey, EphemeralPublicKey)>, output: PrivacyPreservingCircuitOutput, ) -> Result { - if public_keys.len() != output.ciphertexts.len() { - return Err(LeeError::InvalidInput( - "Ephemeral public keys and ciphertexts length mismatch".into(), - )); - } - - let encrypted_private_post_states = output - .ciphertexts - .into_iter() - .zip(public_keys) - .map(|(ciphertext, (npk, vpk, epk))| { - EncryptedAccountData::new(ciphertext, &npk, &vpk, epk) - }) - .collect(); Ok(Self { public_account_ids, nonces, public_post_states: output.public_post_states, - encrypted_private_post_states, + encrypted_private_post_states: output.encrypted_private_post_states, new_commitments: output.new_commitments, new_nullifiers: output.new_nullifiers, block_validity_window: output.block_validity_window, @@ -143,7 +92,7 @@ pub mod tests { Commitment, EncryptionScheme, Nullifier, NullifierPublicKey, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::ViewingPublicKey, program::{BlockValidityWindow, TimestampValidityWindow}, }; use sha2::{Digest as _, Sha256}; @@ -208,7 +157,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 (0u8), to: None (0u8)} + // validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} let unbounded_window_bytes: &[u8] = &[0_u8; 2]; let expected_borsh_vec: Vec = [ @@ -246,13 +195,11 @@ pub mod tests { #[test] fn encrypted_account_data_constructor() { let npk = NullifierPublicKey::from(&[1; 32]); - let vpk = ViewingPublicKey::from_scalar([2; 32]); + let vpk = ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]); let account = Account::default(); let account_id = lee_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 (shared_secret, epk) = SharedSecretKey::encapsulate_deterministic(&vpk, &[0_u8; 32], 0); let ciphertext = EncryptionScheme::encrypt( &account, &PrivateAccountKind::Regular(0), diff --git a/lee/state_machine/src/program.rs b/lee/state_machine/src/program.rs index 9692a5ee..c4223810 100644 --- a/lee/state_machine/src/program.rs +++ b/lee/state_machine/src/program.rs @@ -498,6 +498,20 @@ mod tests { } } + #[test] + fn elf_returns_the_program_bytecode_constant() { + // `Program::elf` must return exactly the compile-time ELF, never an empty + // or placeholder slice. Catches mutations returning `Vec::leak(Vec::new())`, + // `Vec::leak(vec![0])`, or `Vec::leak(vec![1])`. + let at = Program::authenticated_transfer_program(); + assert!(!at.elf().is_empty()); + assert_eq!(at.elf(), AUTHENTICATED_TRANSFER_ELF); + + let token = Program::token(); + assert!(!token.elf().is_empty()); + assert_eq!(token.elf(), TOKEN_ELF); + } + #[test] fn program_execution() { let program = Program::simple_balance_transfer(); diff --git a/lee/state_machine/src/program_deployment_transaction/message.rs b/lee/state_machine/src/program_deployment_transaction/message.rs index a51e4149..866399e8 100644 --- a/lee/state_machine/src/program_deployment_transaction/message.rs +++ b/lee/state_machine/src/program_deployment_transaction/message.rs @@ -16,3 +16,18 @@ impl Message { self.bytecode } } + +#[cfg(test)] +mod tests { + use super::Message; + + #[test] + fn bytecode_roundtrip() { + // `Message::new(b).into_bytecode()` must return exactly `b`. Catches + // mutations of `into_bytecode` returning `vec![]`, `vec![0]`, or `vec![1]`. + let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic + assert_eq!(Message::new(bytecode.clone()).into_bytecode(), bytecode); + assert!(Message::new(vec![]).into_bytecode().is_empty()); + assert_eq!(Message::new(vec![0xAB]).into_bytecode(), vec![0xAB_u8]); + } +} diff --git a/lee/state_machine/src/signature/private_key.rs b/lee/state_machine/src/signature/private_key.rs index 29f3cd3c..c13be154 100644 --- a/lee/state_machine/src/signature/private_key.rs +++ b/lee/state_machine/src/signature/private_key.rs @@ -1,7 +1,9 @@ use std::str::FromStr; +use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; use rand::{Rng as _, rngs::OsRng}; use serde_with::{DeserializeFromStr, SerializeDisplay}; +use sha2::{Digest as _, Sha256}; use crate::error::LeeError; @@ -60,6 +62,29 @@ impl PrivateKey { pub const fn value(&self) -> &[u8; 32] { &self.0 } + + /// `tweak` produces the "tweaked secret key" (`sk`) given a public account's `ssk`. + /// We use "tweaked keys" to shield the public accounts' `ssk` against quantum threats. + /// The "tweaked keys" are used for Schnorr Signatures (BIP-340). + /// The usage of these keys will be greatly reduced once LEE is upgraded to use a PQ signatures. + pub fn tweak(value: &[u8; 32]) -> Result { + if !Self::is_valid_key(*value) { + return Err(LeeError::InvalidPrivateKey); + } + + let sk = k256::SecretKey::from_slice(value).map_err(|_e| LeeError::InvalidPrivateKey)?; + + let hashed: [u8; 32] = + Sha256::digest(sk.public_key().to_encoded_point(true).as_bytes()).into(); + + let sk = sk.to_nonzero_scalar(); + + let scalar = k256::Scalar::from_repr(hashed.into()) + .into_option() + .ok_or(LeeError::InvalidPrivateKey)?; + + Self::try_new(sk.add(&scalar).to_bytes().into()) + } } #[cfg(test)] @@ -75,4 +100,33 @@ mod tests { fn produce_key() { let _key = PrivateKey::new_os_random(); } + + #[test] + fn tweak_rejects_zero_key() { + assert!(matches!( + PrivateKey::tweak(&[0_u8; 32]), + Err(LeeError::InvalidPrivateKey) + )); + } + + // tweak: 0xFF…FF exceeds the secp256k1 curve order + #[test] + fn tweak_rejects_out_of_range_key() { + assert!(matches!( + PrivateKey::tweak(&[0xFF; 32]), + Err(LeeError::InvalidPrivateKey) + )); + } + + #[test] + fn tweak_deterministic() { + let tweaked = PrivateKey::tweak(&[1_u8; 32]).unwrap(); + assert_eq!( + tweaked.value(), + &[ + 242, 210, 33, 19, 65, 108, 136, 176, 179, 128, 110, 210, 107, 193, 168, 112, 206, + 171, 86, 238, 131, 10, 39, 36, 44, 39, 246, 20, 46, 193, 204, 66 + ] + ); + } } diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 33cb12d2..c7152917 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -418,10 +418,10 @@ pub mod tests { use authenticated_transfer_core::Instruction as AuthTransferInstruction; use lee_core::{ - BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, - NullifierSecretKey, SharedSecretKey, Timestamp, + BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier, + NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, - encryption::{EphemeralPublicKey, Scalar, ViewingPublicKey}, + encryption::{EphemeralPublicKey, ViewingPublicKey}, program::{ BlockValidityWindow, ExecutionValidationError, PdaSeed, ProgramId, TimestampValidityWindow, WrappedBalanceSum, @@ -536,7 +536,8 @@ pub mod tests { pub struct TestPrivateKeys { pub nsk: NullifierSecretKey, - pub vsk: Scalar, + pub d: [u8; 32], + pub z: [u8; 32], } impl TestPrivateKeys { @@ -545,7 +546,7 @@ pub mod tests { } pub fn vpk(&self) -> ViewingPublicKey { - ViewingPublicKey::from_scalar(self.vsk) + ViewingPublicKey::from_seed(&self.d, &self.z) } } @@ -612,6 +613,48 @@ pub mod tests { PublicTransaction::new(message, witness_set) } + #[test] + fn genesis_system_accounts_have_expected_contents() { + // System-account IDs must be distinct and non-default, and the genesis + // faucet/bridge accounts must carry their expected field values. Catches + // mutations that replace `system_faucet_account`/`system_bridge_account` + // with `Default::default()`, delete their `balance`/`program_owner` + // fields, or replace `system_bridge_account_id` with `Default::default()`. + let faucet_id = system_faucet_account_id(); + let bridge_id = system_bridge_account_id(); + assert_ne!(bridge_id, AccountId::default()); + assert_ne!(faucet_id, bridge_id); + + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); + let default_owner = Account::default().program_owner; + + let faucet = state.get_account_by_id(faucet_id); + assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX"); + assert_ne!( + faucet.program_owner, default_owner, + "faucet must have a non-default program_owner" + ); + + let bridge = state.get_account_by_id(bridge_id); + assert_ne!( + bridge.program_owner, default_owner, + "bridge must have a non-default program_owner" + ); + } + + #[test] + fn genesis_commitment_set_digest_differs_from_empty_state() { + // The genesis state inserts DUMMY_COMMITMENT, so its commitment-set digest + // must differ from a freshly-created empty state's all-zero root. Catches + // the mutation that replaces `commitment_set_digest` with `Default::default()`. + let genesis = V03State::new_with_genesis_accounts(&[], vec![], 0); + let empty = V03State::new(); + assert_ne!( + genesis.commitment_set_digest(), + empty.commitment_set_digest() + ); + } + #[test] fn new_with_genesis() { let key1 = PrivateKey::try_new([1; 32]).unwrap(); @@ -1333,14 +1376,16 @@ pub mod tests { pub fn test_private_account_keys_1() -> TestPrivateKeys { TestPrivateKeys { nsk: [13; 32], - vsk: [31; 32], + d: [31; 32], + z: [32; 32], } } pub fn test_private_account_keys_2() -> TestPrivateKeys { TestPrivateKeys { nsk: [38; 32], - vsk: [83; 32], + d: [83; 32], + z: [84; 32], } } @@ -1361,9 +1406,8 @@ pub mod tests { let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &recipient_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender, recipient], @@ -1374,6 +1418,11 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateUnauthorized { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), ssk: shared_secret, identifier: 0, @@ -1386,7 +1435,6 @@ pub mod tests { let message = Message::try_from_circuit_output( vec![sender_keys.account_id()], vec![sender_nonce], - vec![(recipient_keys.npk(), recipient_keys.vpk(), epk)], output, ) .unwrap(); @@ -1413,13 +1461,11 @@ pub mod tests { let recipient_pre = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0)); - let esk_1 = [3; 32]; - let shared_secret_1 = SharedSecretKey::new(esk_1, &sender_keys.vpk()); - let epk_1 = EphemeralPublicKey::from_scalar(esk_1); + let (shared_secret_1, epk_1) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); - let esk_2 = [3; 32]; - let shared_secret_2 = SharedSecretKey::new(esk_2, &recipient_keys.vpk()); - let epk_2 = EphemeralPublicKey::from_scalar(esk_2); + let (shared_secret_2, epk_2) = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1429,6 +1475,11 @@ pub mod tests { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: epk_1, + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret_1, nsk: sender_keys.nsk, membership_proof: state @@ -1437,6 +1488,11 @@ pub mod tests { identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: epk_2, + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), ssk: shared_secret_2, identifier: 0, @@ -1446,16 +1502,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - (sender_keys.npk(), sender_keys.vpk(), epk_1), - (recipient_keys.npk(), recipient_keys.vpk(), epk_2), - ], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -1483,9 +1530,8 @@ pub mod tests { *recipient_account_id, ); - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = circuit::execute_and_prove( vec![sender_pre, recipient_pre], @@ -1495,6 +1541,11 @@ pub mod tests { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: state @@ -1508,13 +1559,8 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![*recipient_account_id], - vec![], - vec![(sender_keys.npk(), sender_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = + Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -1631,6 +1677,79 @@ pub mod tests { assert!(state.private_state.1.contains(&expected_new_nullifier)); } + fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + ..Account::default() + }; + let recipient_keys = test_private_account_keys_2(); + let state = V03State::new_with_genesis_accounts(&[], vec![], 0) + .with_private_account(&sender_keys, &sender_private_account); + let tx = private_balance_transfer_for_tests( + &sender_keys, + &sender_private_account, + &recipient_keys, + 37, + &state, + ); + (state, tx) + } + + /// After a valid fully-private tx is proven, tampering with a note's epk should + /// make the shielding proof invalid. + #[test] + fn privacy_tampered_epk_is_rejected() { + use crate::validated_state_diff::ValidatedStateDiff; + + let (state, mut tx) = valid_private_transfer_tx_and_state(); + + // Baseline: the untampered tx verifies + assert!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), + "the unmodified private transfer must verify" + ); + + // Flip a byte of the first note's epk + tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF; + + assert!( + matches!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), + Err(LeeError::InvalidPrivacyPreservingProof) + ), + "a tampered epk must be rejected by proof verification" + ); + } + + /// After a valid fully-private tx is proven, tampering with a note's view tag should + /// make the shielding proof invalid. + #[test] + fn privacy_tampered_view_tag_is_rejected() { + use crate::validated_state_diff::ValidatedStateDiff; + + let (state, mut tx) = valid_private_transfer_tx_and_state(); + + // Baseline: the untampered tx verifies. + assert!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(), + "the unmodified private transfer must verify" + ); + + // Flip the first note's view_tag + tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF; + + assert!( + matches!( + ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0), + Err(LeeError::InvalidPrivacyPreservingProof) + ), + "a tampered view_tag must be rejected by proof verification" + ); + } + #[test] fn transition_from_privacy_preserving_transaction_deshielded() { let sender_keys = test_private_account_keys_1(); @@ -1993,14 +2112,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: recipient_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2039,14 +2178,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2085,14 +2244,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2131,14 +2310,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2177,14 +2376,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2221,14 +2440,34 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([55; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic( + &sender_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, }, InputAccountIdentity::PrivateUnauthorized { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), npk: recipient_keys.npk(), - ssk: SharedSecretKey::new([56; 32], &recipient_keys.vpk()), + ssk: SharedSecretKey::encapsulate_deterministic( + &recipient_keys.vpk(), + &[0_u8; 32], + 0, + ) + .0, identifier: 0, }, ], @@ -2247,7 +2486,8 @@ pub mod tests { let program = Program::simple_balance_transfer(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let public_account_1 = AccountWithMetadata::new( Account { program_owner: program.id(), @@ -2266,6 +2506,8 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: u128::MAX, @@ -2289,7 +2531,8 @@ pub mod tests { 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 shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2298,6 +2541,8 @@ pub mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: u128::MAX, @@ -2309,7 +2554,7 @@ pub mod tests { let (output, _proof) = result.expect("private PDA claim should succeed"); assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.new_commitments.len(), 1); - assert_eq!(output.ciphertexts.len(), 1); + assert_eq!(output.encrypted_private_post_states.len(), 1); assert!(output.public_pre_states.is_empty()); assert!(output.public_post_states.is_empty()); } @@ -2326,7 +2571,8 @@ pub mod tests { let npk_a = keys_a.npk(); let npk_b = keys_b.npk(); let seed = PdaSeed::new([42; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys_b.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; // `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 @@ -2338,6 +2584,8 @@ pub mod tests { vec![pre_state], Program::serialize_instruction(seed).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()), npk: npk_b, ssk: shared_secret, identifier: u128::MAX, @@ -2361,7 +2609,8 @@ pub mod tests { let keys = test_private_account_keys_1(); let npk = keys.npk(); let seed = PdaSeed::new([77; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2374,6 +2623,8 @@ pub mod tests { vec![pre_state], Program::serialize_instruction((seed, seed, callee_id)).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: u128::MAX, @@ -2400,7 +2651,8 @@ pub mod tests { let npk = keys.npk(); let claim_seed = PdaSeed::new([77; 32]); let wrong_delegated_seed = PdaSeed::new([88; 32]); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let account_id = AccountId::for_private_pda(&delegator.id(), &claim_seed, &npk, u128::MAX); let pre_state = AccountWithMetadata::new(Account::default(), false, account_id); @@ -2413,6 +2665,8 @@ pub mod tests { vec![pre_state], Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: u128::MAX, @@ -2438,8 +2692,8 @@ pub mod tests { let keys_a = test_private_account_keys_1(); let keys_b = test_private_account_keys_2(); let seed = PdaSeed::new([55; 32]); - let shared_a = SharedSecretKey::new([66; 32], &keys_a.vpk()); - let shared_b = SharedSecretKey::new([77; 32], &keys_b.vpk()); + let shared_a = SharedSecretKey::encapsulate_deterministic(&keys_a.vpk(), &[0_u8; 32], 0).0; + let shared_b = SharedSecretKey::encapsulate_deterministic(&keys_b.vpk(), &[0_u8; 32], 0).0; 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); @@ -2452,12 +2706,16 @@ pub mod tests { Program::serialize_instruction(seed).unwrap(), vec![ InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()), npk: keys_a.npk(), ssk: shared_a, identifier: u128::MAX, seed: None, }, InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()), npk: keys_b.npk(), ssk: shared_b, identifier: u128::MAX, @@ -2480,7 +2738,8 @@ pub mod tests { let program = Program::noop(); let keys = test_private_account_keys_1(); let npk = keys.npk(); - let shared_secret = SharedSecretKey::new([55; 32], &keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0; let seed = PdaSeed::new([99; 32]); // Simulate a previously-claimed private PDA: program_owner != DEFAULT, is_authorized = @@ -2499,6 +2758,8 @@ pub mod tests { vec![owned_pre_state], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivatePdaInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()), npk, ssk: shared_secret, identifier: u128::MAX, @@ -2580,18 +2841,29 @@ pub mod tests { (&sender_keys.npk(), 0), ); - let shared_secret = SharedSecretKey::new([55; 32], &sender_keys.vpk()); + let shared_secret = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0; let result = execute_and_prove( vec![private_account_1.clone(), private_account_1], Program::serialize_instruction(100_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: (1, vec![]), identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: (1, vec![]), @@ -2924,9 +3196,8 @@ pub mod tests { AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key)); let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0); let balance = 37; @@ -2938,6 +3209,11 @@ pub mod tests { .unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), ssk: shared_secret, nsk: sender_keys.nsk, membership_proof: state @@ -2951,13 +3227,9 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![recipient_account_id], - vec![Nonce(0)], - vec![(sender_keys.npk(), sender_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = + Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -3030,13 +3302,11 @@ pub mod tests { None, ); - let from_esk = [3; 32]; - let from_ss = SharedSecretKey::new(from_esk, &from_keys.vpk()); - let from_epk = EphemeralPublicKey::from_scalar(from_esk); + let (from_ss, from_epk) = + SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0); - let to_esk = [3; 32]; - let to_ss = SharedSecretKey::new(to_esk, &to_keys.vpk()); - let to_epk = EphemeralPublicKey::from_scalar(to_esk); + let (to_ss, to_epk) = + SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1); let mut dependencies = HashMap::new(); @@ -3066,6 +3336,11 @@ pub mod tests { Program::serialize_instruction(instruction).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: to_epk, + view_tag: EncryptedAccountData::compute_view_tag( + &to_keys.npk(), + &to_keys.vpk(), + ), ssk: to_ss, nsk: from_keys.nsk, membership_proof: state @@ -3074,6 +3349,11 @@ pub mod tests { identifier: 0, }, InputAccountIdentity::PrivateAuthorizedUpdate { + epk: from_epk, + view_tag: EncryptedAccountData::compute_view_tag( + &from_keys.npk(), + &from_keys.vpk(), + ), ssk: from_ss, nsk: to_keys.nsk, membership_proof: state @@ -3086,16 +3366,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![ - (to_keys.npk(), to_keys.vpk(), to_epk), - (from_keys.npk(), from_keys.vpk(), from_epk), - ], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); let transaction = PrivacyPreservingTransaction::new(message, witness_set); @@ -3333,9 +3604,8 @@ pub mod tests { let program = Program::authenticated_transfer_program(); // Set up parameters for the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction = authenticated_transfer_core::Instruction::Initialize; @@ -3344,6 +3614,11 @@ pub mod tests { vec![authorized_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &private_keys.npk(), + &private_keys.vpk(), + ), ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, @@ -3353,13 +3628,7 @@ pub mod tests { .unwrap(); // Create message from circuit output - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![(private_keys.npk(), private_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); @@ -3385,14 +3654,18 @@ pub mod tests { AccountWithMetadata::new(Account::default(), false, (&private_keys.npk(), 0)); let program = Program::claimer(); - let esk = [5; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let (output, proof) = execute_and_prove( vec![unauthorized_account], Program::serialize_instruction(0_u128).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &private_keys.npk(), + &private_keys.vpk(), + ), npk: private_keys.npk(), ssk: shared_secret, identifier: 0, @@ -3401,13 +3674,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![(private_keys.npk(), private_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -3435,9 +3702,8 @@ pub mod tests { let claimer_program = Program::claimer(); // Set up parameters for claiming the new account - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &private_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0); let instruction = authenticated_transfer_core::Instruction::Initialize; @@ -3446,6 +3712,11 @@ pub mod tests { vec![authorized_account.clone()], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &private_keys.npk(), + &private_keys.vpk(), + ), ssk: shared_secret, nsk: private_keys.nsk, identifier: 0, @@ -3454,13 +3725,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![(private_keys.npk(), private_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); let tx = PrivacyPreservingTransaction::new(message, witness_set); @@ -3485,14 +3750,19 @@ pub mod tests { }; let noop_program = Program::noop(); - let esk2 = [4; 32]; - let shared_secret2 = SharedSecretKey::new(esk2, &private_keys.vpk()); + let shared_secret2 = + SharedSecretKey::encapsulate_deterministic(&private_keys.vpk(), &[0_u8; 32], 0).0; // Step 3: Try to execute noop program with authentication but without initialization let res = execute_and_prove( vec![account_metadata], Program::serialize_instruction(()).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedInit { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &private_keys.npk(), + &private_keys.vpk(), + ), ssk: shared_secret2, nsk: private_keys.nsk, identifier: 0, @@ -3570,7 +3840,13 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3596,7 +3872,13 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { - ssk: SharedSecretKey::new([3; 32], &sender_keys.vpk()), + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &sender_keys.npk(), + &sender_keys.vpk(), + ), + ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0) + .0, nsk: sender_keys.nsk, membership_proof: (0, vec![]), identifier: 0, @@ -3642,8 +3924,8 @@ pub mod tests { let balance_to_transfer = 10_u128; let instruction = (balance_to_transfer, auth_transfers.id()); - let recipient_esk = [3; 32]; - let recipient = SharedSecretKey::new(recipient_esk, &recipient_keys.vpk()); + let recipient = + SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0; let mut dependencies = HashMap::new(); dependencies.insert(auth_transfers.id(), auth_transfers); @@ -3656,6 +3938,11 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivateAuthorizedUpdate { + epk: EphemeralPublicKey(Vec::new()), + view_tag: EncryptedAccountData::compute_view_tag( + &recipient_keys.npk(), + &recipient_keys.vpk(), + ), ssk: recipient, nsk: recipient_keys.nsk, membership_proof: state @@ -3799,9 +4086,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); let instruction = ( block_validity_window, @@ -3811,6 +4097,11 @@ pub mod tests { vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &account_keys.npk(), + &account_keys.vpk(), + ), npk: account_keys.npk(), ssk: shared_secret, identifier: 0, @@ -3819,13 +4110,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![(account_keys.npk(), account_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) @@ -3869,9 +4154,8 @@ pub mod tests { let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0)); let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0).with_test_programs(); let tx = { - let esk = [3; 32]; - let shared_secret = SharedSecretKey::new(esk, &account_keys.vpk()); - let epk = EphemeralPublicKey::from_scalar(esk); + let (shared_secret, epk) = + SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0); let instruction = ( BlockValidityWindow::new_unbounded(), @@ -3881,6 +4165,11 @@ pub mod tests { vec![pre], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateUnauthorized { + epk, + view_tag: EncryptedAccountData::compute_view_tag( + &account_keys.npk(), + &account_keys.vpk(), + ), npk: account_keys.npk(), ssk: shared_secret, identifier: 0, @@ -3889,13 +4178,7 @@ pub mod tests { ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![], - vec![], - vec![(account_keys.npk(), account_keys.vpk(), epk)], - output, - ) - .unwrap(); + let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); PrivacyPreservingTransaction::new(message, witness_set) @@ -4425,8 +4708,10 @@ pub mod tests { ..Account::default() }; - let alice_shared_0 = SharedSecretKey::new([10; 32], &alice_keys.vpk()); - let alice_shared_1 = SharedSecretKey::new([11; 32], &alice_keys.vpk()); + let (alice_shared_0, alice_epk_0) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 0); + let (alice_shared_1, alice_epk_1) = + SharedSecretKey::encapsulate_deterministic(&alice_keys.vpk(), &[0_u8; 32], 1); // Fund alice_pda_0 via authenticated_transfer directly. { @@ -4442,6 +4727,11 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { + epk: alice_epk_0.clone(), + view_tag: EncryptedAccountData::compute_view_tag( + &alice_npk, + &alice_keys.vpk(), + ), npk: alice_npk, ssk: alice_shared_0, identifier: 0, @@ -4451,17 +4741,9 @@ pub mod tests { &auth_transfer.clone().into(), ) .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 message = + Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); state .transition_from_privacy_preserving_transaction( @@ -4486,6 +4768,11 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaInit { + epk: alice_epk_1.clone(), + view_tag: EncryptedAccountData::compute_view_tag( + &alice_npk, + &alice_keys.vpk(), + ), npk: alice_npk, ssk: alice_shared_1, identifier: 1, @@ -4495,17 +4782,9 @@ pub mod tests { &auth_transfer.into(), ) .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 message = + Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); state .transition_from_privacy_preserving_transaction( @@ -4533,6 +4812,11 @@ pub mod tests { Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { + epk: alice_epk_0, + view_tag: EncryptedAccountData::compute_view_tag( + &alice_npk, + &alice_keys.vpk(), + ), ssk: alice_shared_0, nsk: alice_keys.nsk, membership_proof: state @@ -4546,17 +4830,9 @@ pub mod tests { &spend_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 message = + Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); state .transition_from_privacy_preserving_transaction( @@ -4578,6 +4854,11 @@ pub mod tests { Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), vec![ InputAccountIdentity::PrivatePdaUpdate { + epk: alice_epk_1, + view_tag: EncryptedAccountData::compute_view_tag( + &alice_npk, + &alice_keys.vpk(), + ), ssk: alice_shared_1, nsk: alice_keys.nsk, membership_proof: state @@ -4591,17 +4872,8 @@ pub mod tests { &spend_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 message = + Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[]); state .transition_from_privacy_preserving_transaction( @@ -4626,7 +4898,7 @@ pub mod tests { }; let commitment_pda_1_after_spend = Commitment::new(&alice_pda_1_id, &alice_pda_1_account_after_spend); - let alice_shared_1_refund = SharedSecretKey::new([12; 32], &alice_keys.vpk()); + let alice_shared_1_refund = SharedSecretKey([12; 32]); { let recipient_account = state.get_account_by_id(recipient_id); let recipient_nonce = recipient_account.nonce; @@ -4644,6 +4916,11 @@ pub mod tests { vec![ InputAccountIdentity::Public, InputAccountIdentity::PrivatePdaUpdate { + epk: EphemeralPublicKey(vec![12_u8; 1088]), + view_tag: EncryptedAccountData::compute_view_tag( + &alice_npk, + &alice_keys.vpk(), + ), nsk: alice_keys.nsk, ssk: alice_shared_1_refund, membership_proof: state @@ -4656,17 +4933,9 @@ pub mod tests { &Program::authenticated_transfer_program().into(), ) .unwrap(); - let message = Message::try_from_circuit_output( - vec![recipient_id], - vec![recipient_nonce], - vec![( - alice_npk, - alice_keys.vpk(), - EphemeralPublicKey::from_scalar([12; 32]), - )], - output, - ) - .unwrap(); + let message = + Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output) + .unwrap(); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); state .transition_from_privacy_preserving_transaction( diff --git a/lee/state_machine/src/validated_state_diff.rs b/lee/state_machine/src/validated_state_diff.rs index bf42ad49..0d71d485 100644 --- a/lee/state_machine/src/validated_state_diff.rs +++ b/lee/state_machine/src/validated_state_diff.rs @@ -492,12 +492,7 @@ fn check_privacy_preserving_circuit_proof_is_valid( let output = PrivacyPreservingCircuitOutput { public_pre_states: public_pre_states.to_vec(), public_post_states: message.public_post_states.clone(), - ciphertexts: message - .encrypted_private_post_states - .iter() - .cloned() - .map(|value| value.ciphertext) - .collect(), + encrypted_private_post_states: message.encrypted_private_post_states.clone(), new_commitments: message.new_commitments.clone(), new_nullifiers: message.new_nullifiers.clone(), block_validity_window: message.block_validity_window, @@ -526,6 +521,44 @@ mod tests { validated_state_diff::ValidatedStateDiff, }; + #[test] + fn public_diff_reflects_a_successful_transfer() { + // A successful native transfer must record the debited sender in + // `public_diff()`. Catches the mutation that replaces `public_diff` with + // `HashMap::new()` (which would hide every account change). + use authenticated_transfer_core::Instruction as AtInstruction; + + let from_key = PrivateKey::try_new([1_u8; 32]).unwrap(); + let from = AccountId::from(&PublicKey::new_from_private_key(&from_key)); + let to_key = PrivateKey::try_new([2_u8; 32]).unwrap(); + let to = AccountId::from(&PublicKey::new_from_private_key(&to_key)); + + let state = V03State::new_with_genesis_accounts(&[(from, 100)], vec![], 0); + let program_id = Program::authenticated_transfer_program().id(); + let message = Message::try_new( + program_id, + vec![from, to], + vec![Nonce(0), Nonce(0)], + AtInstruction::Transfer { amount: 5 }, + ) + .unwrap(); + let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]); + let tx = crate::PublicTransaction::new(message, witness_set); + + let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0) + .expect("a valid native transfer must validate"); + let public_diff = diff.public_diff(); + + assert!( + public_diff.contains_key(&from), + "public_diff must contain the debited sender", + ); + assert_eq!( + public_diff[&from].balance, 95, + "sender balance in the diff must reflect the debit", + ); + } + /// Privacy-path version of the authorization-injection attack. The test passes when the /// attack is rejected and the victim's balance is left untouched. /// @@ -542,9 +575,8 @@ mod tests { #[test] fn privacy_malicious_programs_cannot_drain_public_victim() { use lee_core::{ - Commitment, InputAccountIdentity, SharedSecretKey, + Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, - encryption::EphemeralPublicKey, }; use crate::{ @@ -571,9 +603,7 @@ mod tests { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let attacker_esk = [12_u8; 32]; - let attacker_ssk = SharedSecretKey::new(attacker_esk, &attacker_keys.vpk()); - let attacker_epk = EphemeralPublicKey::from_scalar(attacker_esk); + let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); let victim_id = AccountId::new([20_u8; 32]); let recipient_id = AccountId::new([42_u8; 32]); @@ -629,6 +659,11 @@ mod tests { // [2] recipient — first seen in authenticated_transfer's program_output.pre_states let account_identities = vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: attacker_epk, + view_tag: EncryptedAccountData::compute_view_tag( + &attacker_keys.npk(), + &attacker_keys.vpk(), + ), ssk: attacker_ssk, nsk: attacker_keys.nsk, membership_proof, @@ -653,7 +688,6 @@ mod tests { let message = Message::try_from_circuit_output( vec![victim_id, recipient_id], vec![], // no public signers, no nonces - vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)], circuit_output, ) .unwrap(); @@ -693,9 +727,8 @@ mod tests { #[test] fn privacy_malicious_programs_cannot_drain_private_victim() { use lee_core::{ - Commitment, InputAccountIdentity, SharedSecretKey, + Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, - encryption::EphemeralPublicKey, }; use crate::{ @@ -725,9 +758,7 @@ mod tests { // Attacker controls a private account. let attacker_keys = test_private_account_keys_1(); let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0); - let attacker_esk = [12_u8; 32]; - let attacker_ssk = SharedSecretKey::new(attacker_esk, &attacker_keys.vpk()); - let attacker_epk = EphemeralPublicKey::from_scalar(attacker_esk); + let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk()); // Victim is a private account — not registered in public chain state. let victim_keys = test_private_account_keys_2(); @@ -788,6 +819,11 @@ mod tests { // so PrivateAuthorizedUpdate is not an option. let account_identities = vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + epk: attacker_epk, + view_tag: EncryptedAccountData::compute_view_tag( + &attacker_keys.npk(), + &attacker_keys.vpk(), + ), ssk: attacker_ssk, nsk: attacker_keys.nsk, membership_proof, @@ -813,7 +849,6 @@ mod tests { let message = Message::try_from_circuit_output( vec![victim_id, recipient_id], vec![], // no public signers, no nonces - vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)], circuit_output, ) .unwrap(); @@ -936,4 +971,56 @@ mod tests { "recipient should receive nothing" ); } + + /// Regression test: a `PrivacyPreservingTransaction` carrying a structurally invalid + /// proof must be rejected with a clean `Err`. + #[test] + fn privacy_garbage_proof_is_rejected() { + use lee_core::{ + Commitment, + account::Account, + program::{BlockValidityWindow, TimestampValidityWindow}, + }; + + use crate::{ + PrivacyPreservingTransaction, + privacy_preserving_transaction::{ + circuit::Proof, message::Message, witness_set::WitnessSet, + }, + }; + + let state = V03State::new_with_genesis_accounts(&[], vec![], 0); + + // Minimal message that passes every check up to proof verification: a single + // commitment satisfies the non-empty requirement, no signers makes the + // nonce/signature checks vacuously true, and unbounded validity windows are valid + // for any block/timestamp. + let account_id = AccountId::from(&PublicKey::new_from_private_key( + &PrivateKey::try_new([1_u8; 32]).unwrap(), + )); + let commitment = Commitment::new(&account_id, &Account::default()); + let message = Message { + public_account_ids: vec![], + nonces: vec![], + public_post_states: vec![], + encrypted_private_post_states: vec![], + new_commitments: vec![commitment], + new_nullifiers: vec![], + block_validity_window: BlockValidityWindow::new_unbounded(), + timestamp_validity_window: TimestampValidityWindow::new_unbounded(), + }; + + // Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`. + let garbage_proof = Proof::from_inner(vec![0xff_u8; 64]); + let witness_set = WitnessSet::for_message(&message, garbage_proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + + let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0); + + match result { + Err(LeeError::InvalidPrivacyPreservingProof) => {} + Err(other) => panic!("expected InvalidPrivacyPreservingProof, got {other:?}"), + Ok(_) => panic!("garbage proof was accepted instead of rejected"), + } + } } diff --git a/lez/common/src/block.rs b/lez/common/src/block.rs index 8f82c56a..6e956f9f 100644 --- a/lez/common/src/block.rs +++ b/lez/common/src/block.rs @@ -5,14 +5,12 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256, digest::FixedOutput as _}; use crate::{HashType, transaction::LeeTransaction}; -pub type MantleMsgId = [u8; 32]; pub type BlockHash = HashType; #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct BlockMeta { pub id: BlockId, pub hash: BlockHash, - pub msg_id: MantleMsgId, } #[derive(Debug, Clone)] @@ -55,7 +53,6 @@ pub struct Block { pub header: BlockHeader, pub body: BlockBody, pub bedrock_status: BedrockStatus, - pub bedrock_parent_id: MantleMsgId, } impl Serialize for Block { @@ -80,11 +77,7 @@ pub struct HashableBlockData { impl HashableBlockData { #[must_use] - pub fn into_pending_block( - self, - signing_key: &lee::PrivateKey, - bedrock_parent_id: MantleMsgId, - ) -> Block { + pub fn into_pending_block(self, signing_key: &lee::PrivateKey) -> Block { const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Block/\x00\x00\x00\x00\x00\x00\x00\x00"; let data_bytes = borsh::to_vec(&self).unwrap(); @@ -111,7 +104,6 @@ impl HashableBlockData { transactions: self.transactions, }, bedrock_status: BedrockStatus::Pending, - bedrock_parent_id, } } } diff --git a/lez/common/src/config.rs b/lez/common/src/config.rs index c076f699..ec7ed6b3 100644 --- a/lez/common/src/config.rs +++ b/lez/common/src/config.rs @@ -53,3 +53,33 @@ impl From for BasicAuthCredentials { Self::new(value.username, value.password) } } + +#[cfg(test)] +mod tests { + use std::str::FromStr as _; + + use super::BasicAuth; + + #[test] + fn parse_preserves_non_empty_password() { + let auth = BasicAuth::from_str("user:secret").expect("must parse"); + assert_eq!(auth.username, "user"); + assert_eq!(auth.password.as_deref(), Some("secret")); + } + + #[test] + fn parse_empty_password_is_none() { + // A trailing colon means an empty password, which must become `None`. + // Catches deletion of `!` in `.filter(|p| !p.is_empty())`, which would + // instead yield `Some("")`. + let auth = BasicAuth::from_str("user:").expect("must parse"); + assert_eq!(auth.password, None); + } + + #[test] + fn parse_username_only_has_no_password() { + let auth = BasicAuth::from_str("alice").expect("must parse"); + assert_eq!(auth.username, "alice"); + assert_eq!(auth.password, None); + } +} diff --git a/lez/common/src/lib.rs b/lez/common/src/lib.rs index a7744d63..cfbbbd9b 100644 --- a/lez/common/src/lib.rs +++ b/lez/common/src/lib.rs @@ -93,4 +93,16 @@ mod tests { let deserialized = HashType::from_str(&serialized).unwrap(); assert_eq!(original, deserialized); } + + #[test] + fn as_ref_returns_exact_inner_bytes() { + // `HashType::as_ref` must return exactly the inner `[u8; 32]` — not an + // empty slice or a placeholder. Catches mutations of `as_ref` that return + // `Vec::leak(Vec::new())`, `vec![0]`, or `vec![1]`. + let known = [0x42_u8; 32]; + let hash = HashType(known); + assert_eq!(hash.as_ref(), &known); + assert_eq!(hash.as_ref().len(), 32); + assert_eq!(HashType([0_u8; 32]).as_ref().len(), 32); + } } diff --git a/lez/common/src/test_utils.rs b/lez/common/src/test_utils.rs index f4e185ea..179e9601 100644 --- a/lez/common/src/test_utils.rs +++ b/lez/common/src/test_utils.rs @@ -39,7 +39,7 @@ pub fn produce_dummy_block( transactions, }; - block_data.into_pending_block(&sequencer_sign_key_for_testing(), [0; 32]) + block_data.into_pending_block(&sequencer_sign_key_for_testing()) } #[must_use] diff --git a/lez/common/src/transaction.rs b/lez/common/src/transaction.rs index 42a7b761..a74474a9 100644 --- a/lez/common/src/transaction.rs +++ b/lez/common/src/transaction.rs @@ -78,18 +78,9 @@ impl LeeTransaction { block_id: BlockId, timestamp: Timestamp, ) -> Result { - let diff = match self { - Self::Public(tx) => { - ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp) - } - Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction( - tx, state, block_id, timestamp, - ), - Self::ProgramDeployment(tx) => { - ValidatedStateDiff::from_program_deployment_transaction(tx, state) - } - }?; + let diff = self.compute_state_diff(state, block_id, timestamp)?; + // system accounts guard let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([ lee::system_faucet_account_id(), lee::system_bridge_account_id(), @@ -101,6 +92,28 @@ impl LeeTransaction { Ok(diff) } + /// Computes the validated state diff without enforcing the system-account + /// restriction. Shared by [`Self::validate_on_state`] and + /// [`Self::execute_without_system_accounts_check_on_state`]. + fn compute_state_diff( + &self, + state: &V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + match self { + Self::Public(tx) => { + ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp) + } + Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction( + tx, state, block_id, timestamp, + ), + Self::ProgramDeployment(tx) => { + ValidatedStateDiff::from_program_deployment_transaction(tx, state) + } + } + } + /// Validates the transaction against the current state, rejects modifications to clock /// system accounts, and applies the resulting diff to the state. pub fn execute_check_on_state( @@ -115,6 +128,28 @@ impl LeeTransaction { state.apply_state_diff(diff); Ok(self) } + + /// Similar to [`Self::execute_check_on_state`], but skips the system-account guard. + /// + /// FIXME: HOT FIX (testnet v0.2): the indexer replays blocks the sequencer already + /// accepted, including sequencer-generated deposit transactions that + /// legitimately modify the bridge account. The `TransactionOrigin::Sequencer` + /// tag that lets the sequencer bypass the guard is not carried in the block, + /// so the indexer cannot yet distinguish deposit txs from user txs. + /// + /// REMOVE ME when the indexer can authenticate deposit transactions. + pub fn execute_without_system_accounts_check_on_state( + self, + state: &mut V03State, + block_id: BlockId, + timestamp: Timestamp, + ) -> Result { + let diff = self + .compute_state_diff(state, block_id, timestamp) + .inspect_err(|err| warn!("Error at transition {err:#?}"))?; + state.apply_state_diff(diff); + Ok(self) + } } impl From for LeeTransaction { @@ -188,3 +223,47 @@ fn validate_doesnt_modify_account( Ok(()) } } + +#[cfg(test)] +mod tests { + use lee::{ + AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PrivateKey, PublicKey, V03State, + system_bridge_account_id, system_faucet_account_id, + }; + + use crate::test_utils::create_transaction_native_token_transfer; + + #[test] + fn system_account_ids_are_distinct_and_non_default() { + let faucet = system_faucet_account_id(); + let bridge = system_bridge_account_id(); + assert_ne!(faucet, AccountId::default()); + assert_ne!(bridge, AccountId::default()); + assert_ne!(faucet, bridge); + } + + #[test] + fn validate_on_state_rejects_modifying_a_system_account() { + // A native transfer that credits a clock system account *changes* that + // account, so `validate_doesnt_modify_account` must reject it. Catches + // the `!=` → `==` inversion at `validate_doesnt_modify_account` (a changed + // account would no longer be flagged) and `public_diff → HashMap::new()` + // (an empty diff hides the modification). + let sender_key = PrivateKey::try_new([5_u8; 32]).expect("valid key"); + let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000)], vec![], 0); + + let tx = create_transaction_native_token_transfer( + sender_id, + 0, + CLOCK_01_PROGRAM_ACCOUNT_ID, + 100, + &sender_key, + ); + + assert!( + tx.validate_on_state(&state, 1, 0).is_err(), + "validate_on_state must reject a transfer that credits a clock system account", + ); + } +} diff --git a/lez/explorer_service/src/components/block_preview.rs b/lez/explorer_service/src/components/block_preview.rs index 8fe48f9f..0d3b9e14 100644 --- a/lez/explorer_service/src/components/block_preview.rs +++ b/lez/explorer_service/src/components/block_preview.rs @@ -27,7 +27,6 @@ pub fn BlockPreview(block: Block) -> impl IntoView { }, body: BlockBody { transactions }, bedrock_status, - bedrock_parent_id: _, } = block; let tx_count = transactions.len(); diff --git a/lez/explorer_service/src/pages/block_page.rs b/lez/explorer_service/src/pages/block_page.rs index 8f54fe18..5df3b4dc 100644 --- a/lez/explorer_service/src/pages/block_page.rs +++ b/lez/explorer_service/src/pages/block_page.rs @@ -64,7 +64,6 @@ pub fn BlockPage() -> impl IntoView { transactions, }, bedrock_status, - bedrock_parent_id: _, } = blk; let hash_str = hash.to_string(); diff --git a/lez/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index d293637c..f00c94c5 100644 --- a/lez/indexer/core/src/block_store.rs +++ b/lez/indexer/core/src/block_store.rs @@ -171,7 +171,10 @@ impl IndexerStore { transaction .clone() .transaction_stateless_check()? - .execute_check_on_state( + // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to + // sequencer-generated deposit tx'es; + // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions + .execute_without_system_accounts_check_on_state( &mut state_guard, block.header.block_id, block.header.timestamp, @@ -238,10 +241,8 @@ mod tests { timestamp: 0, transactions: vec![clock_tx], }; - let genesis_block = genesis_block_data.into_pending_block( - &common::test_utils::sequencer_sign_key_for_testing(), - [0; 32], - ); + let genesis_block = genesis_block_data + .into_pending_block(&common::test_utils::sequencer_sign_key_for_testing()); let mut prev_hash = Some(genesis_block.header.hash); storage .put_block(genesis_block, HeaderId::from([0_u8; 32])) diff --git a/lez/indexer/ffi/indexer_ffi.h b/lez/indexer/ffi/indexer_ffi.h index 1562eb15..84aaeae7 100644 --- a/lez/indexer/ffi/indexer_ffi.h +++ b/lez/indexer/ffi/indexer_ffi.h @@ -320,13 +320,10 @@ typedef struct 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 { diff --git a/lez/indexer/ffi/src/api/types/block.rs b/lez/indexer/ffi/src/api/types/block.rs index bca2fdb5..e7ae0760 100644 --- a/lez/indexer/ffi/src/api/types/block.rs +++ b/lez/indexer/ffi/src/api/types/block.rs @@ -1,9 +1,7 @@ -use indexer_service_protocol::{ - BedrockStatus, Block, BlockHeader, HashType, MantleMsgId, Signature, -}; +use indexer_service_protocol::{BedrockStatus, Block, BlockHeader, HashType, Signature}; use crate::api::types::{ - FfiBlockId, FfiHashType, FfiMsgId, FfiOption, FfiSignature, FfiTimestamp, FfiVec, + FfiBlockId, FfiHashType, FfiOption, FfiSignature, FfiTimestamp, FfiVec, transaction::free_ffi_transaction_vec, vectors::FfiBlockBody, }; @@ -12,7 +10,6 @@ pub struct FfiBlock { pub header: FfiBlockHeader, pub body: FfiBlockBody, pub bedrock_status: FfiBedrockStatus, - pub bedrock_parent_id: FfiMsgId, } impl From for FfiBlock { @@ -21,7 +18,6 @@ impl From for FfiBlock { header, body, bedrock_status, - bedrock_parent_id, } = value; Self { @@ -33,7 +29,6 @@ impl From for FfiBlock { .collect::>() .into(), bedrock_status: bedrock_status.into(), - bedrock_parent_id: bedrock_parent_id.into(), } } } @@ -126,8 +121,6 @@ pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { #[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); }; @@ -166,8 +159,6 @@ pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { #[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); }; diff --git a/lez/indexer/ffi/src/api/types/mod.rs b/lez/indexer/ffi/src/api/types/mod.rs index b42b014c..0b3574e6 100644 --- a/lez/indexer/ffi/src/api/types/mod.rs +++ b/lez/indexer/ffi/src/api/types/mod.rs @@ -1,4 +1,4 @@ -use indexer_service_protocol::{AccountId, HashType, MantleMsgId, ProgramId, PublicKey, Signature}; +use indexer_service_protocol::{AccountId, HashType, ProgramId, PublicKey, Signature}; pub mod account; pub mod block; @@ -68,7 +68,6 @@ impl From for u128 { } pub type FfiHashType = FfiBytes32; -pub type FfiMsgId = FfiBytes32; pub type FfiBlockId = u64; pub type FfiTimestamp = u64; pub type FfiSignature = FfiBytes64; @@ -82,12 +81,6 @@ impl From for FfiHashType { } } -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 } diff --git a/lez/indexer/service/protocol/src/convert.rs b/lez/indexer/service/protocol/src/convert.rs index cec8b7bb..cd0bff7e 100644 --- a/lez/indexer/service/protocol/src/convert.rs +++ b/lez/indexer/service/protocol/src/convert.rs @@ -4,8 +4,8 @@ use lee_core::account::Nonce; use crate::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, Ciphertext, Commitment, - CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, MantleMsgId, - Nullifier, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, + CommitmentSetDigest, Data, EncryptedAccountData, EphemeralPublicKey, HashType, Nullifier, + PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, WitnessSet, }; @@ -630,14 +630,12 @@ impl From for Block { header, body, bedrock_status, - bedrock_parent_id, } = value; Self { header: header.into(), body: body.into(), bedrock_status: bedrock_status.into(), - bedrock_parent_id: MantleMsgId(bedrock_parent_id), } } } @@ -650,14 +648,12 @@ impl TryFrom for common::block::Block { header, body, bedrock_status, - bedrock_parent_id, } = value; Ok(Self { header: header.try_into()?, body: body.try_into()?, bedrock_status: bedrock_status.into(), - bedrock_parent_id: bedrock_parent_id.0, }) } } diff --git a/lez/indexer/service/protocol/src/lib.rs b/lez/indexer/service/protocol/src/lib.rs index 0d94167d..a670dee6 100644 --- a/lez/indexer/service/protocol/src/lib.rs +++ b/lez/indexer/service/protocol/src/lib.rs @@ -145,7 +145,6 @@ pub struct Block { pub header: BlockHeader, pub body: BlockBody, pub bedrock_status: BedrockStatus, - pub bedrock_parent_id: MantleMsgId, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] @@ -358,13 +357,6 @@ impl FromStr for HashType { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -pub struct MantleMsgId( - #[serde(with = "base64::arr")] - #[schemars(with = "String", description = "base64-encoded Bedrock message id")] - pub [u8; 32], -); - #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub enum BedrockStatus { Pending, diff --git a/lez/indexer/service/src/mock_service.rs b/lez/indexer/service/src/mock_service.rs index a83e9ccc..d9ab9484 100644 --- a/lez/indexer/service/src/mock_service.rs +++ b/lez/indexer/service/src/mock_service.rs @@ -10,10 +10,10 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use indexer_service_protocol::{ Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, - CommitmentSetDigest, Data, EncryptedAccountData, HashType, MantleMsgId, - PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, - ProgramDeploymentTransaction, ProgramId, PublicMessage, PublicTransaction, Signature, - Transaction, ValidityWindow, WitnessSet, + CommitmentSetDigest, Data, EncryptedAccountData, HashType, PrivacyPreservingMessage, + PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, + ProgramId, PublicMessage, PublicTransaction, Signature, Transaction, ValidityWindow, + WitnessSet, }; use jsonrpsee::{ core::{SubscriptionResult, async_trait}, @@ -432,7 +432,6 @@ fn build_mock_block( transactions: block_transactions, }, bedrock_status, - bedrock_parent_id: MantleMsgId([0; 32]), } } diff --git a/lez/keycard_wallet/Cargo.toml b/lez/keycard_wallet/Cargo.toml index 746915cb..4abff1f1 100644 --- a/lez/keycard_wallet/Cargo.toml +++ b/lez/keycard_wallet/Cargo.toml @@ -13,3 +13,4 @@ pyo3.workspace = true log.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +zeroize.workspace = true diff --git a/lez/keycard_wallet/keycard_applets/LEE_keycard.cap b/lez/keycard_wallet/keycard_applets/LEE_keycard.cap index b44835c4..b2e71d56 100644 Binary files a/lez/keycard_wallet/keycard_applets/LEE_keycard.cap and b/lez/keycard_wallet/keycard_applets/LEE_keycard.cap differ diff --git a/lez/keycard_wallet/python/keycard_wallet.py b/lez/keycard_wallet/python/keycard_wallet.py index 7e18636a..21e966cb 100644 --- a/lez/keycard_wallet/python/keycard_wallet.py +++ b/lez/keycard_wallet/python/keycard_wallet.py @@ -3,15 +3,18 @@ from keycard.exceptions import APDUError, TransportError from ecdsa import VerifyingKey, SECP256k1 from keycard.keycard import KeyCard - +from keycard.commands.export_lee_key import export_lee_key from mnemonic import Mnemonic from keycard import constants -import keycard +import os import secrets DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing" +def _pairing_password() -> str: + return os.environ.get("KEYCARD_PAIRING_PASSWORD", DEFAULT_PAIRING_PASSWORD) + class KeycardWallet: def __init__(self): self.card = KeyCard() @@ -37,7 +40,7 @@ class KeycardWallet: return False return True - def initialize(self, pin: str) -> bool: + def initialize(self, pin: str, pairing_password: str | None = None) -> bool: try: self.card.select() @@ -45,14 +48,18 @@ class KeycardWallet: raise RuntimeError("Card is already initialized") puk = ''.join(secrets.choice('0123456789') for _ in range(12)) - self.card.init(pin, puk, DEFAULT_PAIRING_PASSWORD) + self.card.init(pin, puk, pairing_password or _pairing_password()) print(f"Keycard PUK: {puk}") print("Record this PUK and store it somewhere safe. It cannot be recovered.") return True except Exception as e: raise RuntimeError(f"Error initializing keycard: {e}") from e - def setup_communication(self, pin: str, password = DEFAULT_PAIRING_PASSWORD) -> bool: + def _reconnect(self) -> None: + self.card = KeyCard() + self.card.select() + + def _pair(self, pin: str, password: str) -> tuple[int, bytes]: self.card.select() if not self.card.is_initialized: @@ -70,14 +77,28 @@ class KeycardWallet: self.card.unpair(pairing_index) except Exception: pass - raise RuntimeError(f"Error setting up communication: {e}") from e + raise RuntimeError(f"Error opening secure channel after fresh pair: {e}") from e - return True + return pairing_index, pairing_key - def get_pairing_data(self) -> tuple[int, bytes]: - return (self.pairing_index, self.pairing_key) + def pair(self, pin: str, password: str | None = None) -> tuple[int, bytes]: + password = password or _pairing_password() + try: + return self._pair(pin, password) + except TransportError as e: + print(f"Transport error during fresh pair ({e}), attempting card reset and retry...") + try: + self._reconnect() + result = self._pair(pin, password) + print("Retry succeeded after card reset.") + return result + except TransportError as e2: + raise RuntimeError( + "Card lost power and did not recover after reset. " + "Try reseating the card in the reader." + ) from e2 - def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool: + def _setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool: self.card.select() if not self.card.is_initialized: @@ -94,6 +115,22 @@ class KeycardWallet: return True + def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool: + try: + return self._setup_communication_with_pairing(pin, pairing_index, pairing_key) + except TransportError as e: + print(f"Transport error during stored pairing ({e}), attempting card reset and retry...") + try: + self._reconnect() + result = self._setup_communication_with_pairing(pin, pairing_index, pairing_key) + print("Retry succeeded after card reset.") + return result + except TransportError as e2: + raise RuntimeError( + "Card lost power and did not recover after reset. " + "Try reseating the card in the reader." + ) from e2 + def close_session(self) -> bool: return True @@ -161,4 +198,24 @@ class KeycardWallet: return signature.signature except Exception as e: - raise RuntimeError(f"Error signing message: {e}") from e \ No newline at end of file + raise RuntimeError(f"Error signing message: {e}") from e + + def get_private_keys_for_path(self, path: str = "m/44'/60'/0'/0/0") -> bytes | None: + try: + if not self.card.is_secure_channel_open or not self.card.is_pin_verified: + return None + + private_keys = export_lee_key( + self.card, + constants.DerivationOption.DERIVE, + path + ) + + nsk = private_keys.lee_nsk + vsk = private_keys.lee_vsk + + return (nsk, vsk) + + except Exception as e: + raise RuntimeError(f"Error getting private keys: {e}") from e + diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index d7a04544..73486392 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -3,9 +3,14 @@ use std::path::PathBuf; use lee::{AccountId, PublicKey, Signature}; use pyo3::{prelude::*, types::PyAny}; use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; pub mod python_path; +/// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte +/// arrays. +type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 64]>); + // TODO: encrypt at rest alongside broader wallet storage encryption work. #[derive(Serialize, Deserialize)] pub struct KeycardPairingData { @@ -51,10 +56,10 @@ impl KeycardWallet { .extract() } - pub fn get_pairing_data(&self, py: Python<'_>) -> PyResult<(u8, Vec)> { + pub fn pair(&self, py: Python<'_>, pin: &str) -> PyResult<(u8, Vec)> { self.instance .bind(py) - .call_method0("get_pairing_data")? + .call_method1("pair", (pin,))? .extract() } @@ -91,20 +96,11 @@ impl KeycardWallet { { return Ok(()); } - self.setup_communication(py, pin)?; - if let Ok((index, key)) = self.get_pairing_data(py) { - save_pairing(&KeycardPairingData { index, key }); - } + let (index, key) = self.pair(py, pin)?; + save_pairing(&KeycardPairingData { index, key }); Ok(()) } - pub fn setup_communication(&self, py: Python<'_>, pin: &str) -> PyResult { - self.instance - .bind(py) - .call_method1("setup_communication", (pin,))? - .extract() - } - pub fn disconnect(&self, py: Python) -> PyResult { self.instance.bind(py).call_method0("disconnect")?.extract() } @@ -128,7 +124,7 @@ impl KeycardWallet { } pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult { - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py)?; let wallet = Self::new(py)?; wallet.connect(py, pin)?; @@ -138,6 +134,10 @@ impl KeycardWallet { }) } + #[expect( + clippy::arithmetic_side_effects, + reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" + )] pub fn sign_message_for_path( &self, py: Python, @@ -150,6 +150,24 @@ impl KeycardWallet { .call_method1("sign_message_for_path", (message, path))? .extract()?; + // The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. + // Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). + let py_signature = if py_signature.len() < 64 { + if py_signature.len() < 32 { + return Err(PyErr::new::(format!( + "signature from keycard too short: {} bytes", + py_signature.len() + ))); + } + let s_stripped = &py_signature[32..]; + let mut padded = [0_u8; 64]; + padded[..32].copy_from_slice(&py_signature[..32]); + padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); + padded.to_vec() + } else { + py_signature + }; + let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec| { PyErr::new::(format!( "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", @@ -173,7 +191,7 @@ impl KeycardWallet { path: &str, message: &[u8; 32], ) -> PyResult<(Signature, PublicKey)> { - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py)?; let wallet = Self::new(py)?; wallet.connect(py, pin)?; @@ -190,11 +208,65 @@ impl KeycardWallet { Ok(()) } - pub fn get_account_id_for_path_with_connect(pin: &str, key_path: &str) -> PyResult { + pub fn get_public_account_id_for_path_with_connect( + pin: &str, + key_path: &str, + ) -> PyResult { let public_key = Self::get_public_key_for_path_with_connect(pin, key_path)?; Ok(format!("Public/{}", AccountId::from(&public_key))) } + + pub fn get_private_keys_for_path(&self, py: Python, path: &str) -> PyResult { + let (raw_nsk, raw_vsk): (Vec, Vec) = self + .instance + .bind(py) + .call_method1("get_private_keys_for_path", (path,))? + .extract()?; + + let raw_nsk = Zeroizing::new(raw_nsk); + let raw_vsk = Zeroizing::new(raw_vsk); + + let nsk = { + if raw_nsk.len() != 32 { + return Err(PyErr::new::(format!( + "expected 32-byte NSK from keycard, got {} bytes", + raw_nsk.len() + ))); + } + let mut arr = Zeroizing::new([0_u8; 32]); + arr.copy_from_slice(&raw_nsk); + arr + }; + + let vsk = { + if raw_vsk.len() != 64 { + return Err(PyErr::new::(format!( + "expected 64-byte VSK from keycard, got {} bytes", + raw_vsk.len() + ))); + } + let mut arr = Zeroizing::new([0_u8; 64]); + arr.copy_from_slice(&raw_vsk); + arr + }; + + Ok((nsk, vsk)) + } + + pub fn get_private_keys_for_path_with_connect( + pin: &str, + path: &str, + ) -> PyResult { + Python::attach(|py| { + python_path::add_python_path(py)?; + let wallet = Self::new(py)?; + wallet.connect(py, pin)?; + let result = wallet.get_private_keys_for_path(py, path); + drop(wallet.disconnect(py)); + result + }) + } } fn pairing_file_path() -> Option { diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs index 5261d7b7..99ed936e 100644 --- a/lez/keycard_wallet/src/python_path.rs +++ b/lez/keycard_wallet/src/python_path.rs @@ -12,8 +12,12 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .unwrap_or_else(|| current_dir.clone()); let mut paths_to_add: Vec = vec![ - python_base.join("keycard_wallet").join("python"), python_base + .join("lez") + .join("keycard_wallet") + .join("python"), + python_base + .join("lez") .join("keycard_wallet") .join("python") .join("keycard-py"), @@ -44,7 +48,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { let sys = PyModule::import(py, "sys")?; let binding = sys.getattr("path")?; - let sys_path = binding.downcast::()?; + let sys_path = binding.cast::()?; for path in &paths_to_add { let path_str = path.to_str().expect("Invalid path"); diff --git a/lez/keycard_wallet/tests/force_unpower.py b/lez/keycard_wallet/tests/force_unpower.py new file mode 100755 index 00000000..427d2028 --- /dev/null +++ b/lez/keycard_wallet/tests/force_unpower.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +""" +Forces the card in the first available reader into the unpowered state via +PC/SC SCARD_UNPOWER_CARD. Run immediately before a wallet command to simulate +the power-loss condition reported on some USB reader/driver combinations. + +Either: +- pcscd re-powers the card on the next SCardConnect, so wallet +commands will succeed without triggering the retry path. +- the card stays unpowered, triggering TransportError +and exercising the retry wrapper in pair() / setup_communication_with_pairing(). +""" +import sys +from smartcard.scard import ( + SCardEstablishContext, SCardListReaders, SCardConnect, SCardDisconnect, + SCARD_SCOPE_USER, SCARD_SHARE_SHARED, + SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1, + SCARD_UNPOWER_CARD, +) + +hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) +hresult, reader_list = SCardListReaders(hcontext, []) + +if not reader_list: + print("force_unpower: no readers found, skipping.") + sys.exit(0) + +hresult, hcard, _ = SCardConnect( + hcontext, + reader_list[0], + SCARD_SHARE_SHARED, + SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, +) + +if hresult != 0: + print(f"force_unpower: SCardConnect failed (hresult={hresult:#010x}), skipping.") + sys.exit(0) + +SCardDisconnect(hcard, SCARD_UNPOWER_CARD) +print("force_unpower: card powered down.") diff --git a/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh b/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh new file mode 100755 index 00000000..3d8301f7 --- /dev/null +++ b/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# Power-recovery variant of keycard_tests.sh. +# +# Forces a card power cycle before each keycard-backed wallet command to verify +# commands survive mid-session power loss. + +source venv/bin/activate + +export KEYCARD_PIN=111111 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +unpower() { + python "$SCRIPT_DIR/force_unpower.py" +} + +echo "Test: wallet keycard available" +wallet keycard available + +echo "" +echo "Test: wallet keycard load (after power cycle)" +export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then" +unpower +wallet keycard load +unset KEYCARD_MNEMONIC + +echo "" +echo "Test: wallet auth-transfer init --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet auth-transfer init --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet pinata claim --to \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet pinata claim --to "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet auth-transfer init and send between two keycard accounts (after power cycle)" +unpower +wallet auth-transfer init --account-id "m/44'/60'/0'/0/1" +unpower +wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/1" + +echo "" +echo "Test: create local wallet account" +LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+') +echo "Created local account: Public/${LOCAL_ACCOUNT_ID}" + +echo "" +echo "Test: wallet auth-transfer init local account" +wallet auth-transfer init --account-id "Public/${LOCAL_ACCOUNT_ID}" + +echo "" +echo "Test: wallet auth-transfer send from keycard to local account (after power cycle)" +unpower +wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/${LOCAL_ACCOUNT_ID}" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\" (after power cycle)" +unpower +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +echo "" +echo "Test: wallet auth-transfer send from local account to keycard account (after power cycle)" +unpower +wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1" + +echo "" +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\" (after power cycle)" +unpower +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/1" + +echo "" +echo "Test: wallet auth-transfer send from keycard to foreign account (after power cycle)" +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" +unpower +wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" + +echo "" +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)" +unpower +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "Test: wallet account get foreign account (after power cycle)" +unpower +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" diff --git a/lez/keycard_wallet/tests/keycard_test_3.sh b/lez/keycard_wallet/tests/keycard_test_3.sh new file mode 100755 index 00000000..d80e2aca --- /dev/null +++ b/lez/keycard_wallet/tests/keycard_test_3.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# keycard_test_3.sh — tests for `wallet keycard get-private-keys`. +# +# Prerequisites: +# 1. Run wallet_with_keycard.sh once to install dependencies. +# 2. Keycard reader inserted with card loaded (wallet keycard load has been run). + +source venv/bin/activate + +cargo install --path lez/wallet --force --features keycard-debug + +export KEYCARD_PIN=111111 + +echo "=== Test: wallet keycard get-private-keys path 10 ===" +wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/10" --reveal + +echo "=== Test: wallet keycard get-private-keys path 11 ===" +wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/11" --reveal + +echo "" +echo "=== All get-private-keys tests finished ===" diff --git a/lez/keycard_wallet/tests/keycard_tests.sh b/lez/keycard_wallet/tests/keycard_tests.sh index e5ac2f2c..dfa30461 100755 --- a/lez/keycard_wallet/tests/keycard_tests.sh +++ b/lez/keycard_wallet/tests/keycard_tests.sh @@ -28,7 +28,8 @@ wallet pinata claim --to "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "m/44'/60'/0'/0/0" -echo "Test: wallet auth-transfer init and send between two keycard accounts" +echo "" +echo "=== Test: Keycard account to Keycard account ===" wallet auth-transfer init --account-id "m/44'/60'/0'/0/1" wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1" @@ -38,7 +39,8 @@ wallet account get --account-id "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" wallet account get --account-id "m/44'/60'/0'/0/1" -# Send from keycard account to a local wallet account +echo "" +echo "=== Test: Keycard account to public local account ===" echo "Test: create local wallet account" LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+') echo "Created local account: Public/${LOCAL_ACCOUNT_ID}" @@ -56,7 +58,8 @@ wallet account get --account-id "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\"" wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" -# Create a local wallet account, fund it, and send to keycard account (co-signed: local key + keycard) +echo "" +echo "=== Test: public local account to Keycard account ===" echo "Test: wallet auth-transfer send from local account to keycard account" wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1" @@ -67,7 +70,8 @@ wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" wallet account get --account-id "m/44'/60'/0'/0/1" -# Send from keycard account to a local wallet account (foreign recipient — no signature needed) +echo "" +echo "=== Test: Keycard account to foreign recipient (no signature required) ===" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" @@ -79,3 +83,50 @@ wallet account get --account-id "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" + +echo "" +echo "=== Test: Shielded auth-transfer to owned private account ===" + +SHIELDED_RECV=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1) +echo "Private recipient: $SHIELDED_RECV" +SHIELDED_KEYS=$(wallet account show-keys --account-id "$SHIELDED_RECV") +SHIELDED_NPK=$(echo "$SHIELDED_KEYS" | head -1) +SHIELDED_VPK=$(echo "$SHIELDED_KEYS" | tail -1) + +wallet auth-transfer send --amount 2 \ + --from "m/44'/60'/0'/0/0" \ + --to-npk "$SHIELDED_NPK" \ + --to-vpk "$SHIELDED_VPK" +echo "Shielded auth-transfer sent" + +sleep 15 +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "" +echo "=== Test: Deshielded auth-transfer: private account → keycard path 1 ===" + +PRIV_SENDER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1) +echo "Fresh private sender account: $PRIV_SENDER" + +wallet auth-transfer init --account-id "$PRIV_SENDER" + +echo "Test: wallet pinata claim to private sender" +wallet pinata claim --to "$PRIV_SENDER" + +sleep 15 + +echo "priv-sender state after claim:" +wallet account get --account-id "$PRIV_SENDER" + +wallet auth-transfer send \ + --from "$PRIV_SENDER" \ + --to "m/44'/60'/0'/0/1" \ + --amount 5 +echo "Deshielded transfer of 5: $PRIV_SENDER → keycard path 1" + +sleep 15 + +echo "priv-sender state (balance should have decreased by 5):" +wallet account get --account-id "$PRIV_SENDER" +echo "Keycard path 1 state (balance should have increased by 5):" +wallet account get --account-id "m/44'/60'/0'/0/1" diff --git a/lez/keycard_wallet/tests/keycard_tests_2.sh b/lez/keycard_wallet/tests/keycard_tests_2.sh new file mode 100755 index 00000000..cbff19fe --- /dev/null +++ b/lez/keycard_wallet/tests/keycard_tests_2.sh @@ -0,0 +1,457 @@ +#!/usr/bin/env bash +# keycard_tests_2.sh — comprehensive token + AMM keycard integration tests. +# +# Prerequisites: +# 1. Run wallet_with_keycard.sh once to install dependencies. +# 2. Reset the local chain so all accounts are uninitialized. +# 3. Keycard reader inserted with card loaded. +# +# Keycard path layout: +# path 2 → LEZ token definition (keycard) +# path 3 → LEZ token supply (keycard) +# path 4 → LEE token definition (keycard) +# path 5 → LEE token supply (keycard) +# path 6 → LEZ holding (keycard — transfers, mint, burn, swap, liquidity) +# path 7 → LEE holding (keycard — swap, add/remove liquidity) +# path 8 → LP holding (keycard — add/remove liquidity) +# path 9 → ATA owner (keycard — ATA create, send, burn) +# +# Non-keycard accounts: +# pub-receiver → public account (target for keycard → public token transfer) +# priv-receiver → private account (target for keycard → private token transfer) +# amm-lez-fund → public LEZ holding used to seed the AMM pool +# amm-lee-fund → public LEE holding used to seed the AMM pool +# (LP holding for amm new is created fresh each run — no persistent label) + +source venv/bin/activate +export KEYCARD_PIN=111111 + +# ============================================================================= +# Keycard setup +# ============================================================================= +echo "" +echo "=== Keycard setup ===" +wallet keycard available +export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then" +wallet keycard load +unset KEYCARD_MNEMONIC + +# ============================================================================= +# Create non-keycard wallet accounts +# ============================================================================= +echo "" +echo "=== Create non-keycard accounts ===" +wallet account new public --label pub-receiver 2>/dev/null || true + +wallet account new public --label amm-lez-fund 2>/dev/null || true +wallet account new public --label amm-lee-fund 2>/dev/null || true +wallet account new public --label amm-lp-fund 2>/dev/null || true + +# ============================================================================= +# (1) Create LEZ token — definition AND supply via keycard paths +# ============================================================================= +echo "" +echo "=== (1) Create LEZ token (keycard def=path2, supply=path3) ===" +wallet token new \ + --definition-account-id "m/44'/60'/0'/0/2" \ + --supply-account-id "m/44'/60'/0'/0/3" \ + --name LEZ \ + --total-supply 100000 +echo "LEZ token created" + +# ============================================================================= +# (2) Create LEE token — definition AND supply via keycard paths +# ============================================================================= +echo "" +echo "=== (2) Create LEE token (keycard def=path4, supply=path5) ===" +wallet token new \ + --definition-account-id "m/44'/60'/0'/0/4" \ + --supply-account-id "m/44'/60'/0'/0/5" \ + --name LEE \ + --total-supply 100000 +echo "LEE token created" + +sleep 15 + +LEZ_DEF_ID=$(wallet account id --account-id "m/44'/60'/0'/0/2") +LEE_DEF_ID=$(wallet account id --account-id "m/44'/60'/0'/0/4") +echo "LEZ definition ID: $LEZ_DEF_ID" +echo "LEE definition ID: $LEE_DEF_ID" + +echo "Keycard path 2 (LEZ definition) state:" +wallet account get --account-id "m/44'/60'/0'/0/2" +echo "Keycard path 3 (LEZ supply) state:" +wallet account get --account-id "m/44'/60'/0'/0/3" +echo "Keycard path 4 (LEE definition) state:" +wallet account get --account-id "m/44'/60'/0'/0/4" +echo "Keycard path 5 (LEE supply) state:" +wallet account get --account-id "m/44'/60'/0'/0/5" + +# ============================================================================= +# Initialize token holding accounts +# ============================================================================= +echo "" +echo "=== Initialize token holding accounts ===" + +# Keycard path 6: LEZ holding (mint 0 to initialize) +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 0 +echo "LEZ holding initialized for keycard path 6" + +# Keycard path 7: LEE holding (different definition — safe to submit immediately) +wallet token mint \ + --definition "m/44'/60'/0'/0/4" \ + --holder "m/44'/60'/0'/0/7" \ + --amount 0 +echo "LEE holding initialized for keycard path 7" + +# Wait for path2 (LEZ def) and path4 (LEE def) nonces to be confirmed before reusing them +sleep 15 + +# pub-receiver: public LEZ holding +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder pub-receiver \ + --amount 0 +echo "LEZ holding initialized for pub-receiver" + +# amm-lee-fund: LEE holding (different definition — safe to submit with pub-receiver) +wallet token mint \ + --definition "m/44'/60'/0'/0/4" \ + --holder amm-lee-fund \ + --amount 0 +echo "LEE holding initialized for amm-lee-fund" + +# Wait for path2 nonce to be confirmed before the third LEZ mint +sleep 15 + +# amm-lez-fund: LEZ holding +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder amm-lez-fund \ + --amount 0 +echo "AMM seed holdings initialized" + +# ============================================================================= +# Fund keycard holdings and AMM seed accounts from supply +# ============================================================================= +echo "" +echo "=== Fund keycard holdings and AMM seed accounts ===" + +wallet token send \ + --from "m/44'/60'/0'/0/3" \ + --to "m/44'/60'/0'/0/6" \ + --amount 20000 +echo "Transferred 20000 LEZ → keycard path 6" + +wallet token send \ + --from "m/44'/60'/0'/0/5" \ + --to "m/44'/60'/0'/0/7" \ + --amount 20000 +echo "Transferred 20000 LEE → keycard path 7" + +# Wait for path3 and path5 nonces to be confirmed before reusing them +sleep 15 + +wallet token send \ + --from "m/44'/60'/0'/0/3" \ + --to amm-lez-fund \ + --amount 10000 +echo "Transferred 10000 LEZ → amm-lez-fund" + +wallet token send \ + --from "m/44'/60'/0'/0/5" \ + --to amm-lee-fund \ + --amount 10000 +echo "Transferred 10000 LEE → amm-lee-fund" + +sleep 15 + +echo "Keycard path 6 (LEZ holding) state (balance should be 20000):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "Keycard path 7 (LEE holding) state (balance should be 20000):" +wallet account get --account-id "m/44'/60'/0'/0/7" +echo "amm-lez-fund state (balance should be 10000):" +wallet account get --account-id amm-lez-fund +echo "amm-lee-fund state (balance should be 10000):" +wallet account get --account-id amm-lee-fund + +# ============================================================================= +# (3) Token transfer: keycard path 6 (LEZ) → public account +# ============================================================================= +echo "" +echo "=== (3) Token transfer: keycard path 6 → pub-receiver (public) ===" +wallet token send \ + --from "m/44'/60'/0'/0/6" \ + --to pub-receiver \ + --amount 1000 +echo "Transferred 1000 LEZ: keycard path 6 → pub-receiver" + +sleep 15 + +echo "Keycard path 6 (LEZ) state (balance should be 19000):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "pub-receiver state (balance should be 1000):" +wallet account get --account-id pub-receiver + +# ============================================================================= +# (4) Token transfer: keycard path 6 (LEZ) → private account (shielded) +# ============================================================================= +echo "" +echo "=== (4) Token transfer: keycard path 6 → priv-receiver (private, shielded) ===" +PRIV_RECEIVER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1) +echo "Fresh private receiver account: $PRIV_RECEIVER" + +wallet token send \ + --from "m/44'/60'/0'/0/6" \ + --to "$PRIV_RECEIVER" \ + --amount 500 +echo "Shielded transfer of 500 LEZ: keycard path 6 → $PRIV_RECEIVER" + +wallet account sync-private + +sleep 15 + +echo "Keycard path 6 (LEZ) state (balance should be 18500):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "priv-receiver state (balance should be 500):" +wallet account get --account-id "$PRIV_RECEIVER" + +# ============================================================================= +# (5) Token transfer: private account → keycard path 6 (deshielded) +# Uses priv-receiver from test (4) which holds 500 LEZ. +# The private sender is handled by the ZK circuit; the keycard recipient +# does not sign — resolve() derives its account ID from the card only. +# ============================================================================= +echo "" +echo "=== (5) Token transfer: priv-receiver (private) → keycard path 6 (deshielded) ===" + +wallet token send \ + --from "$PRIV_RECEIVER" \ + --to "m/44'/60'/0'/0/6" \ + --amount 300 +echo "Deshielded transfer of 300 LEZ: $PRIV_RECEIVER → keycard path 6" + +wallet account sync-private + +sleep 15 + +echo "priv-receiver state (balance should be 200):" +wallet account get --account-id "$PRIV_RECEIVER" +echo "Keycard path 6 (LEZ) state (balance should be 18800):" +wallet account get --account-id "m/44'/60'/0'/0/6" + +# ============================================================================= +# (6) Token mint with keycard — definition signed by keycard path 2 +# ============================================================================= +echo "" +echo "=== (6) Token mint: keycard def path 2 mints 2000 LEZ to keycard path 6 ===" +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 2000 +echo "Minted 2000 LEZ to keycard path 6" + +sleep 15 + +echo "Keycard path 2 (LEZ definition) state (total supply should have increased):" +wallet account get --account-id "m/44'/60'/0'/0/2" +echo "Keycard path 6 (LEZ holding) state (balance should be 20800):" +wallet account get --account-id "m/44'/60'/0'/0/6" + +# ============================================================================= +# (7) Token burn with keycard — holder is keycard path 6 +# ============================================================================= +echo "" +echo "=== (7) Token burn: keycard path 6 burns 500 LEZ ===" +wallet token burn \ + --definition "Public/$LEZ_DEF_ID" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 500 +echo "Burned 500 LEZ from keycard path 6" + +sleep 15 + +echo "Keycard path 2 (LEZ definition) state (total supply should reflect burn):" +wallet account get --account-id "m/44'/60'/0'/0/2" +echo "Keycard path 6 (LEZ holding) state (balance should be 20300):" +wallet account get --account-id "m/44'/60'/0'/0/6" + +# ============================================================================= +# (8) Create AMM pool for LEZ/LEE — without keycard +# ============================================================================= +echo "" +echo "=== (8) Create AMM pool for LEZ/LEE (without keycard) ===" + +wallet amm new \ + --user-holding-a amm-lez-fund \ + --user-holding-b amm-lee-fund \ + --user-holding-lp amm-lp-fund \ + --balance-a 10000 \ + --balance-b 10000 +echo "AMM pool created for LEZ/LEE" + +sleep 15 + +echo "amm-lez-fund state (balance should be 0 — contributed to pool):" +wallet account get --account-id amm-lez-fund +echo "amm-lee-fund state (balance should be 0 — contributed to pool):" +wallet account get --account-id amm-lee-fund +echo "Initial LP holding state (should hold initial LP tokens):" +wallet account get --account-id amm-lp-fund +LP_DEF_ID=$(wallet account get --account-id amm-lp-fund | grep -o '"definition_id":"[^"]*"' | awk -F'"' '{print $4}') +echo "LP token definition ID: $LP_DEF_ID" + +# ============================================================================= +# (9) Swap tokens owned by keycard accounts +# keycard path 7 (LEE) sells 500 LEE; keycard path 6 (LEZ) receives LEZ +# ============================================================================= +echo "" +echo "=== (9) Swap: keycard path 7 sells 500 LEE, keycard path 6 receives LEZ ===" +wallet amm swap-exact-input \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --amount-in 500 \ + --min-amount-out 1 \ + --token-definition "$LEE_DEF_ID" +echo "Swap LEE → LEZ complete via keycard" + +sleep 15 + +echo "Keycard path 6 (LEZ holding) state (balance should have increased):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "Keycard path 7 (LEE holding) state (balance should have decreased by 500):" +wallet account get --account-id "m/44'/60'/0'/0/7" + +# ============================================================================= +# (10) Add liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8) +# ============================================================================= +echo "" +echo "=== (10) Add liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ===" +wallet amm add-liquidity \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ + --max-amount-a 1000 \ + --max-amount-b 1000 \ + --min-amount-lp 1 +echo "Add liquidity complete via keycard" + +sleep 15 + +echo "Keycard path 6 (LEZ holding) state (balance should have decreased):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "Keycard path 7 (LEE holding) state (balance should have decreased):" +wallet account get --account-id "m/44'/60'/0'/0/7" +echo "Keycard path 8 (LP holding) state (should have received LP tokens):" +wallet account get --account-id "m/44'/60'/0'/0/8" + +# ============================================================================= +# (11) Remove liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8) +# ============================================================================= +echo "" +echo "=== (11) Remove liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ===" +wallet amm remove-liquidity \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ + --balance-lp 500 \ + --min-amount-a 1 \ + --min-amount-b 1 +echo "Remove liquidity complete via keycard" + +sleep 15 + +echo "Keycard path 6 (LEZ holding) state (balance should have increased):" +wallet account get --account-id "m/44'/60'/0'/0/6" +echo "Keycard path 7 (LEE holding) state (balance should have increased):" +wallet account get --account-id "m/44'/60'/0'/0/7" +echo "Keycard path 8 (LP holding) state (balance should have decreased):" +wallet account get --account-id "m/44'/60'/0'/0/8" + +# ============================================================================= +# (12) ATA create — keycard path 9 as owner for LEZ +# ============================================================================= +echo "" +echo "=== (12) ATA create: keycard path 9 as owner, LEZ token ===" +ATA_OWNER_ID=$(wallet account id --account-id "m/44'/60'/0'/0/9") +echo "ATA owner (keycard path 9): $ATA_OWNER_ID" + +wallet ata create \ + --owner "m/44'/60'/0'/0/9" \ + --token-definition "$LEZ_DEF_ID" +echo "ATA created for keycard path 9 / LEZ" + +sleep 15 + +LEZ_ATA_ID=$(wallet ata address --owner "$ATA_OWNER_ID" --token-definition "$LEZ_DEF_ID") +echo "Keycard path 9 LEZ ATA ID: $LEZ_ATA_ID" +echo "ATA state (should be initialized with zero balance):" +wallet account get --account-id "Public/$LEZ_ATA_ID" + +# Fund the ATA from LEZ supply (path 3) — setup for tests 12 and 13 +wallet token send \ + --from "m/44'/60'/0'/0/3" \ + --to "Public/$LEZ_ATA_ID" \ + --amount 3000 +echo "Funded keycard path 9 ATA with 3000 LEZ" + +sleep 15 + +echo "ATA state after funding (balance should be 3000):" +wallet account get --account-id "Public/$LEZ_ATA_ID" + +# ============================================================================= +# (13) ATA send — keycard path 9's ATA → pub-receiver's ATA +# ============================================================================= +echo "" +echo "=== (13) ATA send: keycard path 9's ATA → pub-receiver's ATA ===" +PUB_RECEIVER_ID=$(wallet account id --account-id pub-receiver) +wallet ata create \ + --owner "Public/$PUB_RECEIVER_ID" \ + --token-definition "$LEZ_DEF_ID" +echo "ATA created for pub-receiver / LEZ" + +sleep 15 + +PUB_RECEIVER_ATA_ID=$(wallet ata address --owner "$PUB_RECEIVER_ID" --token-definition "$LEZ_DEF_ID") +echo "pub-receiver LEZ ATA ID: $PUB_RECEIVER_ATA_ID" +echo "pub-receiver ATA state (should be initialized with zero balance):" +wallet account get --account-id "Public/$PUB_RECEIVER_ATA_ID" + +wallet ata send \ + --from "m/44'/60'/0'/0/9" \ + --token-definition "$LEZ_DEF_ID" \ + --to "$PUB_RECEIVER_ATA_ID" \ + --amount 500 +echo "Sent 500 LEZ: keycard path 9 ATA → pub-receiver ATA" + +sleep 15 + +echo "Keycard path 9 ATA state (balance should be 2500):" +wallet account get --account-id "Public/$LEZ_ATA_ID" +echo "pub-receiver ATA state (balance should be 500):" +wallet account get --account-id "Public/$PUB_RECEIVER_ATA_ID" + +# ============================================================================= +# (14) ATA burn — keycard path 9's ATA burns 200 LEZ +# ============================================================================= +echo "" +echo "=== (14) ATA burn: keycard path 9's ATA burns 200 LEZ ===" +wallet ata burn \ + --holder "m/44'/60'/0'/0/9" \ + --token-definition "$LEZ_DEF_ID" \ + --amount 200 +echo "Burned 200 LEZ from keycard path 9 ATA" + +sleep 15 + +echo "Keycard path 9 ATA state (balance should be 2300):" +wallet account get --account-id "Public/$LEZ_ATA_ID" +echo "LEZ definition state (total supply should reflect burn):" +wallet account get --account-id "m/44'/60'/0'/0/2" + +echo "" +echo "=== All keycard token + AMM + ATA tests finished ===" diff --git a/lez/sequencer/core/Cargo.toml b/lez/sequencer/core/Cargo.toml index ea08c6f1..f7296f42 100644 --- a/lez/sequencer/core/Cargo.toml +++ b/lez/sequencer/core/Cargo.toml @@ -34,6 +34,7 @@ borsh.workspace = true bytesize.workspace = true hex.workspace = true url.workspace = true +risc0-zkvm.workspace = true [features] default = [] diff --git a/lez/sequencer/core/src/block_publisher.rs b/lez/sequencer/core/src/block_publisher.rs index 62cd7259..f07a47c6 100644 --- a/lez/sequencer/core/src/block_publisher.rs +++ b/lez/sequencer/core/src/block_publisher.rs @@ -94,7 +94,7 @@ impl BlockPublisherTrait for ZoneSdkPublisher { continue; }; match event { - Event::Published { checkpoint, .. } => on_checkpoint(checkpoint), + Event::Checkpoint { checkpoint } => on_checkpoint(checkpoint), Event::TxsFinalized { items } => { for op in items.into_iter().flat_map(|item| item.ops) { match op { @@ -111,7 +111,10 @@ impl BlockPublisherTrait for ZoneSdkPublisher { } } } - Event::ChannelUpdate { .. } | Event::Ready => {} + Event::ChannelUpdate { .. } + | Event::Published { .. } + | Event::Readiness { .. } + | Event::TurnNotification { .. } => {} } } }); diff --git a/lez/sequencer/core/src/block_store.rs b/lez/sequencer/core/src/block_store.rs index 2e934d2c..8a2e9265 100644 --- a/lez/sequencer/core/src/block_store.rs +++ b/lez/sequencer/core/src/block_store.rs @@ -3,14 +3,14 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use anyhow::{Context as _, Result}; use common::{ HashType, - block::{Block, BlockMeta, MantleMsgId}, + block::{Block, BlockMeta}, transaction::LeeTransaction, }; use lee::V03State; use log::info; use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint; pub use storage::DbResult; -use storage::sequencer::RocksDBIO; +use storage::sequencer::{RocksDBIO, sequencer_cells::PendingDepositEventRecord}; pub struct SequencerStore { dbio: Arc, @@ -56,16 +56,10 @@ impl SequencerStore { pub fn create_db_with_genesis( location: &Path, genesis_block: &Block, - genesis_msg_id: MantleMsgId, genesis_state: &V03State, signing_key: lee::PrivateKey, ) -> DbResult { - let dbio = Arc::new(RocksDBIO::create( - location, - genesis_block, - genesis_msg_id, - genesis_state, - )?); + let dbio = Arc::new(RocksDBIO::create(location, genesis_block, genesis_state)?); let genesis_id = dbio.get_meta_first_block_in_db()?; let tx_hash_to_block_map = block_to_transactions_map(genesis_block); @@ -134,14 +128,9 @@ impl SequencerStore { self.dbio.get_all_blocks() } - pub(crate) fn update( - &mut self, - block: &Block, - msg_id: MantleMsgId, - state: &V03State, - ) -> DbResult<()> { + pub(crate) fn update(&mut self, block: &Block, state: &V03State) -> DbResult<()> { let new_transactions_map = block_to_transactions_map(block); - self.dbio.atomic_update(block, msg_id, state)?; + self.dbio.atomic_update(block, state)?; self.tx_hash_to_block_map.extend(new_transactions_map); Ok(()) } @@ -165,6 +154,27 @@ impl SequencerStore { self.dbio.put_zone_sdk_checkpoint_bytes(&bytes)?; Ok(()) } + + pub fn get_unfulfilled_deposit_events(&self) -> DbResult> { + self.dbio.get_pending_deposit_events() + } + + pub fn mark_unfulfilled_deposit_events_submitted( + &self, + deposit_op_ids: &[HashType], + submitted_block_id: u64, + ) -> DbResult { + self.dbio + .mark_pending_deposit_events_submitted(deposit_op_ids, submitted_block_id) + } + + pub fn remove_fulfilled_unfulfilled_deposit_events_up_to_block( + &self, + finalized_block_id: u64, + ) -> DbResult { + self.dbio + .remove_fulfilled_pending_deposit_events_up_to_block(finalized_block_id) + } } pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap { @@ -199,12 +209,11 @@ mod tests { transactions: vec![], }; - let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key); // Start an empty node store let mut node_store = SequencerStore::create_db_with_genesis( path, &genesis_block, - [0; 32], &testnet_initial_state::initial_state(), signing_key, ) @@ -218,7 +227,7 @@ mod tests { assert_eq!(None, retrieved_tx); // Add the block with the transaction let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); - node_store.update(&block, [1; 32], &dummy_state).unwrap(); + node_store.update(&block, &dummy_state).unwrap(); // Try again let retrieved_tx = node_store.get_transaction_by_hash(tx.hash()); assert_eq!(Some(tx), retrieved_tx); @@ -238,13 +247,12 @@ mod tests { transactions: vec![], }; - let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key); let genesis_hash = genesis_block.header.hash; let node_store = SequencerStore::create_db_with_genesis( path, &genesis_block, - [0; 32], &testnet_initial_state::initial_state(), signing_key, ) @@ -253,7 +261,6 @@ mod tests { // Verify that initially the latest block hash equals genesis hash let latest_meta = node_store.latest_block_meta().unwrap(); assert_eq!(latest_meta.hash, genesis_hash); - assert_eq!(latest_meta.msg_id, [0; 32]); } #[test] @@ -270,11 +277,10 @@ mod tests { transactions: vec![], }; - let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key); let mut node_store = SequencerStore::create_db_with_genesis( path, &genesis_block, - [0; 32], &testnet_initial_state::initial_state(), signing_key, ) @@ -284,17 +290,13 @@ mod tests { let tx = common::test_utils::produce_dummy_empty_transaction(); let block = common::test_utils::produce_dummy_block(1, None, vec![tx]); let block_hash = block.header.hash; - let block_msg_id = [1; 32]; let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); - node_store - .update(&block, block_msg_id, &dummy_state) - .unwrap(); + node_store.update(&block, &dummy_state).unwrap(); - // Verify that the latest block meta now equals the new block's hash and msg_id + // Verify that the latest block meta now equals the new block's hash let latest_meta = node_store.latest_block_meta().unwrap(); assert_eq!(latest_meta.hash, block_hash); - assert_eq!(latest_meta.msg_id, block_msg_id); } #[test] @@ -311,11 +313,10 @@ mod tests { transactions: vec![], }; - let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key); let mut node_store = SequencerStore::create_db_with_genesis( path, &genesis_block, - [0; 32], &testnet_initial_state::initial_state(), signing_key, ) @@ -327,7 +328,7 @@ mod tests { let block_id = block.header.block_id; let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0); - node_store.update(&block, [1; 32], &dummy_state).unwrap(); + node_store.update(&block, &dummy_state).unwrap(); // Verify initial status is Pending let retrieved_block = node_store.get_block_at_id(block_id).unwrap().unwrap(); @@ -361,14 +362,13 @@ mod tests { transactions: vec![], }; - let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key); let tx = common::test_utils::produce_dummy_empty_transaction(); { // Create a scope to drop the first store after creating the db let mut node_store = SequencerStore::create_db_with_genesis( path, &genesis_block, - [0; 32], &testnet_initial_state::initial_state(), signing_key.clone(), ) @@ -377,11 +377,7 @@ mod tests { // Add a new block let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]); node_store - .update( - &block, - [1; 32], - &V03State::new_with_genesis_accounts(&[], vec![], 0), - ) + .update(&block, &V03State::new_with_genesis_accounts(&[], vec![], 0)) .unwrap(); } diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index f4084c6d..2a8af528 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -1,4 +1,4 @@ -use std::{path::Path, time::Instant}; +use std::{path::Path, sync::Arc, time::Instant}; use anyhow::{Context as _, Result, anyhow}; use borsh::BorshDeserialize; @@ -16,6 +16,7 @@ use mempool::{MemPool, MemPoolHandle}; #[cfg(feature = "mock")] pub use mock::SequencerCoreWithMockClients; pub use storage::error::DbError; +use storage::sequencer::sequencer_cells::PendingDepositEventRecord; use crate::{ block_publisher::{BlockPublisherTrait, ZoneSdkPublisher}, @@ -95,10 +96,6 @@ impl SequencerCore { db_path.display() ); - // TODO: Remove msg_id from BlockMeta — it is no longer needed now that - // zone-sdk manages L1 settlement state via its own checkpoint. - let genesis_msg_id = [0; 32]; - let genesis_parent_msg_id = [0; 32]; let (genesis_state, genesis_txs) = build_genesis_state(&config); let hashable_data = HashableBlockData { @@ -107,13 +104,11 @@ impl SequencerCore { prev_block_hash: HashType([0; 32]), timestamp: 0, }; - let genesis_block = - hashable_data.into_pending_block(&signing_key, genesis_parent_msg_id); + let genesis_block = hashable_data.into_pending_block(&signing_key); let store = SequencerStore::create_db_with_genesis( &db_path, &genesis_block, - genesis_msg_id, &genesis_state, signing_key, ) @@ -147,26 +142,72 @@ impl SequencerCore { let dbio_for_finalized = store.dbio(); let on_finalized_block: block_publisher::FinalizedBlockSink = Box::new(move |block_id| { + // NOTE: Theoretically Zone SDK may report finalization happening multiple times for the + // same block. In practice this is very unlikely to happen. For that to + // happen Sequencer should crash between receiving Finalized and Checkpoint events while + // these events happen very fast (because Checkpoints are generated by Zone SDK + // locally). + 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:#}"); } + + match dbio_for_finalized.remove_fulfilled_pending_deposit_events_up_to_block(block_id) { + Ok(0) => {} + Ok(removed) => { + info!( + "Removed {removed} fulfilled pending deposit events up to finalized block {block_id}" + ); + } + Err(err) => { + error!( + "Failed to remove fulfilled pending deposit events up to block {block_id}: {err:#}" + ); + } + } }); let (mempool, mempool_handle) = MemPool::new(config.mempool_max_size); + replay_unfulfilled_deposit_events(&store, mempool_handle.clone()); + let mempool_handle_for_deposit = mempool_handle.clone(); + let dbio_for_deposit = store.dbio(); let on_deposit_event: block_publisher::OnDepositEventSink = Box::new(move |deposit| { + // NOTE: Theoretically Zone SDK may report multiple identical deposits. In practice this + // is very unlikely to happen. For that to happen Sequencer should crash + // between receiving Deposit and Checkpoint events while these events happen + // very fast (because Checkpoints are generated by Zone SDK locally). + + let dbio_for_deposit = Arc::clone(&dbio_for_deposit); let mempool_handle_for_deposit = mempool_handle_for_deposit.clone(); Box::pin(async move { - info!( - "Observed Bedrock Deposit event with id: {:?}", - hex::encode(deposit.op_id) - ); - let tx = match build_bridge_deposit_tx(&deposit) { + let id_hex = hex::encode(deposit.op_id); + info!("Observed Bedrock Deposit event with id: {id_hex}"); + + let event_record = pending_deposit_event_record(&deposit); + + match dbio_for_deposit.add_pending_deposit_event(event_record.clone()) { + Ok(true) => {} + Ok(false) => { + info!( + "Deposit event {id_hex} already persisted as unfulfilled, skipping duplicate enqueue", + ); + return; + } + Err(err) => { + error!( + "Failed to persist unfulfilled deposit event {id_hex} before enqueue: {err:#}. Deposit will be lost.", + ); + return; + } + } + + let tx = match build_bridge_deposit_tx_from_event(&event_record) { Ok(tx) => tx, Err(err) => { - warn!( - "Skipping finalized Bedrock deposit event due to tx build failure: {err:#}" + error!( + "Failed to build transaction from Bedrock deposit event {id_hex}: {err:#}. Deposit will be lost.", ); return; } @@ -177,7 +218,7 @@ impl SequencerCore { .await { error!( - "Failed to queue sequencer transaction built from finalized Bedrock event: {err:#}" + "Failed to queue sequencer transaction built from finalized Bedrock event: {err:#}. Deposit will be lost." ); } }) @@ -199,8 +240,11 @@ impl SequencerCore { // 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:#}"); + if is_fresh_start { + block_publisher + .publish_block(&genesis_block) + .await + .expect("Failed to publish genesis block"); } let sequencer_core = Self { @@ -217,30 +261,43 @@ impl SequencerCore { /// Produces a new block from mempool transactions and publishes it via zone-sdk. pub async fn produce_new_block(&mut self) -> Result { - let block = self + let block_with_meta = self .build_block_from_mempool() .context("Failed to build block from mempool transactions")?; + let BlockWithMeta { + block, + deposit_event_ids, + } = block_with_meta; - // 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]; + self.block_publisher + .publish_block(&block) + .await + .context("Failed to publish block to Bedrock")?; - 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, &self.state)?; + + let updated_deposits = self + .store + .mark_unfulfilled_deposit_events_submitted(&deposit_event_ids, block.header.block_id)?; + if updated_deposits > 0 { + info!( + "Marked {updated_deposits} pending deposit events as submitted in block {}", + block.header.block_id + ); } - self.store.update(&block, placeholder_msg_id, &self.state)?; Ok(self.chain_height) } /// 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 { + fn build_block_from_mempool(&mut self) -> Result { let now = Instant::now(); let new_block_height = self.next_block_id(); - let mut valid_transactions = vec![]; + let mut valid_transactions = Vec::new(); + let mut deposit_event_ids = Vec::new(); let max_block_size = usize::try_from(self.sequencer_config.max_block_size.as_u64()) .expect("`max_block_size` should fit into usize"); @@ -311,6 +368,20 @@ impl SequencerCore { let LeeTransaction::Public(public_tx) = &tx else { panic!("Sequencer may only generate Public transactions, found {tx:#?}"); }; + + if public_tx.message.program_id == Program::bridge().id() { + let instruction: bridge_core::Instruction = + risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) + .context("Failed to deserialize bridge instruction")?; + match instruction { + bridge_core::Instruction::Deposit { + l1_deposit_op_id, .. + } => { + deposit_event_ids.push(HashType(l1_deposit_op_id)); + } + } + } + self.state .transition_from_public_transaction( public_tx, @@ -341,12 +412,9 @@ impl SequencerCore { timestamp: new_block_timestamp, }; - // TODO: Remove bedrock_parent_id from Block — it is no longer needed now - // that zone-sdk manages the inscription parent chain internally. - let placeholder_parent_id = [0_u8; 32]; let block = hashable_data .clone() - .into_pending_block(self.store.signing_key(), placeholder_parent_id); + .into_pending_block(self.store.signing_key()); self.chain_height = new_block_height; @@ -355,7 +423,11 @@ impl SequencerCore { hashable_data.transactions.len(), now.elapsed().as_secs() ); - Ok(block) + + Ok(BlockWithMeta { + block, + deposit_event_ids, + }) } pub const fn state(&self) -> &lee::V03State { @@ -411,6 +483,60 @@ impl SequencerCore { } } +struct BlockWithMeta { + block: Block, + deposit_event_ids: Vec, +} + +/// Checks the database for any pending deposit events that have not yet been marked as submitted in +/// a block, and re-queues them in the mempool in a separate async task for inclusion in the next +/// block. +fn replay_unfulfilled_deposit_events( + store: &SequencerStore, + mempool_handle: MemPoolHandle<(TransactionOrigin, LeeTransaction)>, +) { + let replay_records: Vec = store + .get_unfulfilled_deposit_events() + .expect("Failed to load unfulfilled deposit events") + .into_iter() + .filter(|record| record.submitted_in_block_id.is_none()) + .collect(); + + if replay_records.is_empty() { + return; + } + + info!( + "Found {} unfulfilled deposit events in DB, re-queueing", + replay_records.len() + ); + tokio::spawn(async move { + for record in replay_records { + let tx = match build_bridge_deposit_tx_from_event(&record) { + Ok(tx) => tx, + Err(err) => { + warn!( + "Skipping replay of pending deposit event {} due to tx build failure: {err:#}", + hex::encode(record.deposit_op_id) + ); + continue; + } + }; + + if let Err(err) = mempool_handle + .push((TransactionOrigin::Sequencer, tx)) + .await + { + error!( + "Failed to re-queue unfulfilled deposit event {} from DB: {err:#}", + hex::encode(record.deposit_op_id) + ); + break; + } + } + }); +} + /// Builds the initial genesis state from `testnet_initial_state` plus configured genesis /// transactions. Returns the final state and the list of [`LeeTransaction`]s that should be /// committed to the genesis block so external observers can replay them. @@ -485,10 +611,20 @@ fn build_supply_bridge_account_genesis_transaction(balance: u128) -> PublicTrans PublicTransaction::new(message, witness_set) } -fn build_bridge_deposit_tx( +fn pending_deposit_event_record( deposit: &logos_blockchain_zone_sdk::state::DepositInfo, -) -> Result { - let metadata = DepositMetadata::decode(&deposit.metadata) +) -> PendingDepositEventRecord { + PendingDepositEventRecord { + deposit_op_id: HashType(deposit.op_id), + source_tx_hash: HashType(deposit.tx_hash.0), + amount: deposit.amount, + metadata: deposit.metadata.clone().into(), + submitted_in_block_id: None, + } +} + +fn build_bridge_deposit_tx_from_event(event: &PendingDepositEventRecord) -> Result { + let metadata = DepositMetadata::decode(&event.metadata) .context("Failed to decode finalized Bedrock deposit metadata")?; let bridge_program_id = Program::bridge().id(); @@ -501,9 +637,10 @@ fn build_bridge_deposit_tx( vec![lee::system_bridge_account_id(), recipient_vault_id], vec![], bridge_core::Instruction::Deposit { + l1_deposit_op_id: event.deposit_op_id.0, vault_program_id, recipient_id: metadata.recipient_id, - amount: u128::from(deposit.amount), + amount: u128::from(event.amount), }, ) .context("Failed to build bridge deposit message")?; @@ -552,6 +689,7 @@ mod tests { }; use logos_blockchain_core::mantle::ops::channel::ChannelId; use mempool::MemPoolHandle; + use storage::sequencer::sequencer_cells::PendingDepositEventRecord; use tempfile::tempdir; use testnet_initial_state::{initial_accounts, initial_pub_accounts_private_keys}; @@ -563,6 +701,11 @@ mod tests { mock::SequencerCoreWithMockClients, }; + #[derive(borsh::BorshSerialize)] + struct DepositMetadataForEncoding { + recipient_id: lee::AccountId, + } + fn setup_sequencer_config() -> SequencerConfig { let tempdir = tempfile::tempdir().unwrap(); let home = tempdir.path().to_path_buf(); @@ -620,6 +763,35 @@ mod tests { (sequencer, mempool_handle) } + fn tx_is_bridge_deposit( + tx: &LeeTransaction, + deposit_op_id: [u8; 32], + expected_amount: u64, + ) -> bool { + let LeeTransaction::Public(public_tx) = tx else { + return false; + }; + + if public_tx.message.program_id != lee::program::Program::bridge().id() { + return false; + } + + let instruction: bridge_core::Instruction = + match risc0_zkvm::serde::from_slice(&public_tx.message.instruction_data) { + Ok(instruction) => instruction, + Err(_err) => return false, + }; + + matches!( + instruction, + bridge_core::Instruction::Deposit { + l1_deposit_op_id, + amount, + .. + } if l1_deposit_op_id == deposit_op_id && amount == u128::from(expected_amount) + ) + } + #[tokio::test] async fn start_from_config() { let config = setup_sequencer_config(); @@ -654,13 +826,11 @@ mod tests { prev_block_hash: HashType([0; 32]), timestamp: 0, }; - let genesis_block = genesis_hashable_data.into_pending_block(&signing_key, [0; 32]); + let genesis_block = genesis_hashable_data.into_pending_block(&signing_key); - let expected_msg_id = [7; 32]; SequencerStore::create_db_with_genesis( &config.home.join("rocksdb"), &genesis_block, - expected_msg_id, &genesis_state, signing_key, ) @@ -668,10 +838,8 @@ mod tests { let (sequencer, _mempool_handle) = SequencerCoreWithMockClients::start_from_config(config).await; - let latest_meta = sequencer.store.latest_block_meta().unwrap(); - - assert_eq!(latest_meta.msg_id, expected_msg_id); assert_eq!(sequencer.chain_height, 1); + assert!(sequencer.store.latest_block_meta().is_ok()); } #[should_panic(expected = "Failed to open database")] @@ -690,6 +858,69 @@ mod tests { let _ = SequencerCoreWithMockClients::start_from_config(config).await; } + #[tokio::test] + async fn start_from_config_replays_unfulfilled_deposit_events_from_db() { + let config = setup_sequencer_config(); + let deposit_op_id = [13_u8; 32]; + let expected_amount = 1_u64; + let recipient_id = initial_accounts()[0].account_id; + + { + let (_sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config.clone()).await; + } + + let pending_event = PendingDepositEventRecord { + deposit_op_id: HashType(deposit_op_id), + source_tx_hash: HashType([7_u8; 32]), + amount: expected_amount, + metadata: borsh::to_vec(&DepositMetadataForEncoding { recipient_id }).unwrap(), + submitted_in_block_id: None, + }; + + { + let signing_key = lee::PrivateKey::try_new(config.signing_key).unwrap(); + let store = SequencerStore::open_db(&config.home.join("rocksdb"), signing_key).unwrap(); + + let inserted = store + .dbio() + .add_pending_deposit_event(pending_event) + .unwrap(); + assert!(inserted); + } + + let (mut sequencer, _mempool_handle) = + SequencerCoreWithMockClients::start_from_config(config).await; + + let (origin, tx) = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some((origin, tx)) = sequencer.mempool.pop() { + return (origin, tx); + } + + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timed out waiting for pending deposit event to be replayed into mempool"); + + match origin { + TransactionOrigin::Sequencer => {} + TransactionOrigin::User => { + panic!("Unexpected user transaction in empty mempool replay test") + } + } + + assert!(tx_is_bridge_deposit(&tx, deposit_op_id, expected_amount)); + + let pending_events = sequencer.store.get_unfulfilled_deposit_events().unwrap(); + let replayed_event = pending_events + .into_iter() + .find(|event| event.deposit_op_id == HashType(deposit_op_id)) + .expect("Pending deposit event should remain in DB until included in a block"); + assert!(replayed_event.submitted_in_block_id.is_none()); + } + #[test] fn transaction_pre_check_pass() { let tx = common::test_utils::produce_dummy_empty_transaction(); diff --git a/lez/storage/src/indexer/mod.rs b/lez/storage/src/indexer/mod.rs index 51df8042..b682a4d7 100644 --- a/lez/storage/src/indexer/mod.rs +++ b/lez/storage/src/indexer/mod.rs @@ -208,7 +208,10 @@ impl RocksDBIO { "transaction pre check failed with err {err:?}" )) })? - .execute_check_on_state( + // FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to + // sequencer-generated deposit tx'es; + // CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions + .execute_without_system_accounts_check_on_state( &mut breakpoint, block.header.block_id, block.header.timestamp, diff --git a/lez/storage/src/sequencer/mod.rs b/lez/storage/src/sequencer/mod.rs index 01c1343d..803e6d60 100644 --- a/lez/storage/src/sequencer/mod.rs +++ b/lez/storage/src/sequencer/mod.rs @@ -1,6 +1,9 @@ use std::{path::Path, sync::Arc}; -use common::block::{BedrockStatus, Block, BlockMeta, MantleMsgId}; +use common::{ + HashType, + block::{BedrockStatus, Block, BlockMeta}, +}; use lee::V03State; use rocksdb::{ BoundColumnFamily, ColumnFamilyDescriptor, DBWithThreadMode, MultiThreaded, Options, WriteBatch, @@ -12,7 +15,8 @@ use crate::{ error::DbError, sequencer::sequencer_cells::{ LEEStateCellOwned, LEEStateCellRef, LastFinalizedBlockIdCell, LatestBlockMetaCellOwned, - LatestBlockMetaCellRef, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, + LatestBlockMetaCellRef, PendingDepositEventRecord, PendingDepositEventsCellOwned, + PendingDepositEventsCellRef, ZoneSdkCheckpointCellOwned, ZoneSdkCheckpointCellRef, }, }; @@ -24,6 +28,9 @@ pub const DB_META_LAST_FINALIZED_BLOCK_ID: &str = "last_finalized_block_id"; 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 queued deposit events that were not yet +/// fulfilled on L2. +pub const DB_META_PENDING_DEPOSIT_EVENTS_KEY: &str = "pending_deposit_events"; /// Key base for storing the LEE state. pub const DB_LEE_STATE_KEY: &str = "lee_state"; @@ -47,12 +54,7 @@ impl RocksDBIO { Self::open_inner(path, &db_opts) } - pub fn create( - path: &Path, - genesis_block: &Block, - genesis_msg_id: MantleMsgId, - genesis_state: &V03State, - ) -> DbResult { + pub fn create(path: &Path, genesis_block: &Block, genesis_state: &V03State) -> DbResult { let mut db_opts = Options::default(); db_opts.create_missing_column_families(true); db_opts.create_if_missing(true); @@ -62,14 +64,13 @@ impl RocksDBIO { if !is_start_set { let block_id = genesis_block.header.block_id; // TODO: Shouldn't this be atomic (batched)? - dbio.put_meta_first_block_in_db(genesis_block, genesis_msg_id)?; + dbio.put_meta_first_block_in_db(genesis_block)?; dbio.put_meta_is_first_block_set()?; dbio.put_meta_last_block_in_db(block_id)?; dbio.put_meta_last_finalized_block_id(None)?; dbio.put_meta_latest_block_meta(&BlockMeta { id: genesis_block.header.block_id, hash: genesis_block.header.hash, - msg_id: genesis_msg_id, })?; dbio.put_lee_state_in_db(genesis_state)?; } @@ -161,7 +162,7 @@ impl RocksDBIO { self.put_batch(&LEEStateCellRef(state), (), batch) } - pub fn put_meta_first_block_in_db(&self, block: &Block, msg_id: MantleMsgId) -> DbResult<()> { + pub fn put_meta_first_block_in_db(&self, block: &Block) -> DbResult<()> { let cf_meta = self.meta_column(); self.db .put_cf( @@ -182,7 +183,7 @@ impl RocksDBIO { .map_err(|rerr| DbError::rocksdb_cast_message(rerr, None))?; let mut batch = WriteBatch::default(); - self.put_block(block, msg_id, true, &mut batch)?; + self.put_block(block, true, &mut batch)?; self.db.write(batch).map_err(|rerr| { DbError::rocksdb_cast_message( rerr, @@ -239,13 +240,73 @@ impl RocksDBIO { self.put(&ZoneSdkCheckpointCellRef(bytes), ()) } - pub fn put_block( + pub fn get_pending_deposit_events(&self) -> DbResult> { + Ok(self + .get_opt::(())? + .map_or_else(Vec::new, |cell| cell.0)) + } + + fn put_pending_deposit_events(&self, records: &[PendingDepositEventRecord]) -> DbResult<()> { + self.put(&PendingDepositEventsCellRef(records), ()) + } + + pub fn add_pending_deposit_event(&self, event: PendingDepositEventRecord) -> DbResult { + let mut records = self.get_pending_deposit_events()?; + if records + .iter() + .any(|record| record.deposit_op_id == event.deposit_op_id) + { + return Ok(false); + } + records.push(event); + self.put_pending_deposit_events(&records)?; + Ok(true) + } + + pub fn mark_pending_deposit_events_submitted( &self, - block: &Block, - msg_id: MantleMsgId, - first: bool, - batch: &mut WriteBatch, - ) -> DbResult<()> { + deposit_op_ids: &[HashType], + submitted_block_id: u64, + ) -> DbResult { + let mut records = self.get_pending_deposit_events()?; + let mut updated: usize = 0; + + for record in records + .iter_mut() + .filter(|record| deposit_op_ids.contains(&record.deposit_op_id)) + { + record.submitted_in_block_id = Some(submitted_block_id); + updated = updated.saturating_add(1); + } + + if updated > 0 { + self.put_pending_deposit_events(&records)?; + } + + Ok(updated) + } + + pub fn remove_fulfilled_pending_deposit_events_up_to_block( + &self, + finalized_block_id: u64, + ) -> DbResult { + let mut records = self.get_pending_deposit_events()?; + let before = records.len(); + records.retain(|record| { + record + .submitted_in_block_id + .is_none_or(|submitted_id| submitted_id > finalized_block_id) + }); + + let removed = before.saturating_sub(records.len()); + if removed > 0 { + self.put_pending_deposit_events(&records)?; + } + + Ok(removed) + } + + pub fn put_block(&self, block: &Block, first: bool, batch: &mut WriteBatch) -> DbResult<()> { let cf_block = self.block_column(); if !first { @@ -257,7 +318,6 @@ impl RocksDBIO { &BlockMeta { id: block.header.block_id, hash: block.header.hash, - msg_id, }, batch, )?; @@ -379,15 +439,10 @@ impl RocksDBIO { }) } - pub fn atomic_update( - &self, - block: &Block, - msg_id: MantleMsgId, - state: &V03State, - ) -> DbResult<()> { + pub fn atomic_update(&self, block: &Block, state: &V03State) -> DbResult<()> { let block_id = block.header.block_id; let mut batch = WriteBatch::default(); - self.put_block(block, msg_id, false, &mut batch)?; + self.put_block(block, false, &mut batch)?; self.put_lee_state_in_db_batch(state, &mut batch)?; self.db.write(batch).map_err(|rerr| { DbError::rocksdb_cast_message( diff --git a/lez/storage/src/sequencer/sequencer_cells.rs b/lez/storage/src/sequencer/sequencer_cells.rs index 9a6a607b..39b6a406 100644 --- a/lez/storage/src/sequencer/sequencer_cells.rs +++ b/lez/storage/src/sequencer/sequencer_cells.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use common::block::BlockMeta; +use common::{HashType, block::BlockMeta}; use lee::V03State; use crate::{ @@ -8,7 +8,8 @@ use crate::{ error::DbError, sequencer::{ CF_LEE_STATE_NAME, DB_LEE_STATE_KEY, DB_META_LAST_FINALIZED_BLOCK_ID, - DB_META_LATEST_BLOCK_META_KEY, DB_META_ZONE_SDK_CHECKPOINT_KEY, + DB_META_LATEST_BLOCK_META_KEY, DB_META_PENDING_DEPOSIT_EVENTS_KEY, + DB_META_ZONE_SDK_CHECKPOINT_KEY, }, }; @@ -131,12 +132,56 @@ impl SimpleWritableCell for ZoneSdkCheckpointCellRef<'_> { } } +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub struct PendingDepositEventRecord { + pub deposit_op_id: HashType, + pub source_tx_hash: HashType, + pub amount: u64, + pub metadata: Vec, + /// Set when block containing the deposit event is submitted, but not necessarily finalized. + pub submitted_in_block_id: Option, +} + +#[derive(BorshDeserialize)] +pub struct PendingDepositEventsCellOwned(pub Vec); + +impl SimpleStorableCell for PendingDepositEventsCellOwned { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_PENDING_DEPOSIT_EVENTS_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleReadableCell for PendingDepositEventsCellOwned {} + +#[derive(BorshSerialize)] +pub struct PendingDepositEventsCellRef<'records>(pub &'records [PendingDepositEventRecord]); + +impl SimpleStorableCell for PendingDepositEventsCellRef<'_> { + type KeyParams = (); + + const CELL_NAME: &'static str = DB_META_PENDING_DEPOSIT_EVENTS_KEY; + const CF_NAME: &'static str = CF_META_NAME; +} + +impl SimpleWritableCell for PendingDepositEventsCellRef<'_> { + fn value_constructor(&self) -> DbResult> { + borsh::to_vec(&self).map_err(|err| { + DbError::borsh_cast_message( + err, + Some("Failed to serialize pending deposit events cell".to_owned()), + ) + }) + } +} + #[cfg(test)] mod uniform_tests { use crate::{ cells::SimpleStorableCell as _, sequencer::sequencer_cells::{ LEEStateCellOwned, LEEStateCellRef, LatestBlockMetaCellOwned, LatestBlockMetaCellRef, + PendingDepositEventsCellOwned, PendingDepositEventsCellRef, }, }; @@ -165,4 +210,20 @@ mod uniform_tests { LatestBlockMetaCellOwned::key_constructor(()).unwrap() ); } + + #[test] + fn pending_deposit_events_ref_and_owned_is_aligned() { + assert_eq!( + PendingDepositEventsCellRef::CELL_NAME, + PendingDepositEventsCellOwned::CELL_NAME + ); + assert_eq!( + PendingDepositEventsCellRef::CF_NAME, + PendingDepositEventsCellOwned::CF_NAME + ); + assert_eq!( + PendingDepositEventsCellRef::key_constructor(()).unwrap(), + PendingDepositEventsCellOwned::key_constructor(()).unwrap() + ); + } } diff --git a/lez/testnet_initial_state/src/lib.rs b/lez/testnet_initial_state/src/lib.rs index 488c8580..5bb6e1b4 100644 --- a/lez/testnet_initial_state/src/lib.rs +++ b/lez/testnet_initial_state/src/lib.rs @@ -2,10 +2,10 @@ use common::PINATA_BASE58; use key_protocol::key_management::{ KeyChain, key_tree::chain_index::ChainIndex, - secret_holders::{PrivateKeyHolder, SecretSpendingKey}, + secret_holders::{PrivateKeyHolder, SecretSpendingKey, ViewingSecretKey}, }; use lee::{Account, AccountId, Data, PrivateKey, PublicKey, V03State}; -use lee_core::{NullifierPublicKey, encryption::shared_key_derivation::Secp256k1Point}; +use lee_core::{NullifierPublicKey, encryption::ViewingPublicKey}; use serde::{Deserialize, Serialize}; const PRIVATE_KEY_PUB_ACC_A: [u8; 32] = [ @@ -38,24 +38,24 @@ const NSK_PRIV_ACC_B: [u8; 32] = [ 23, 99, 9, 4, 177, 230, 125, 109, 91, 160, 30, ]; -const VSK_PRIV_ACC_A: [u8; 32] = [ - 5, 85, 114, 119, 141, 187, 202, 170, 122, 253, 198, 81, 150, 8, 155, 21, 192, 65, 24, 124, 116, - 98, 110, 106, 137, 90, 165, 239, 80, 13, 222, 30, +const VSK_D_PRIV_ACC_A: [u8; 32] = [ + 255, 250, 140, 26, 222, 223, 174, 95, 132, 108, 124, 88, 30, 247, 82, 72, 52, 70, 84, 139, 241, + 187, 41, 163, 19, 231, 232, 122, 225, 55, 134, 184, ]; -const VSK_PRIV_ACC_B: [u8; 32] = [ - 205, 32, 76, 251, 255, 236, 96, 119, 61, 111, 65, 100, 75, 218, 12, 22, 17, 170, 55, 226, 21, - 154, 161, 34, 208, 74, 27, 1, 119, 13, 88, 128, +const VSK_Z_PRIV_ACC_A: [u8; 32] = [ + 225, 24, 98, 78, 31, 203, 175, 248, 213, 17, 133, 207, 10, 135, 132, 151, 59, 184, 5, 81, 28, + 238, 137, 62, 233, 227, 99, 17, 236, 159, 244, 63, ]; -const VPK_PRIV_ACC_A: [u8; 33] = [ - 2, 210, 206, 38, 213, 4, 182, 198, 220, 47, 93, 148, 61, 84, 148, 250, 158, 45, 8, 81, 48, 80, - 46, 230, 87, 210, 47, 204, 76, 58, 214, 167, 81, +const VSK_D_PRIV_ACC_B: [u8; 32] = [ + 128, 85, 85, 103, 226, 218, 119, 56, 60, 252, 31, 113, 232, 215, 156, 2, 159, 247, 156, 192, + 12, 178, 229, 236, 255, 120, 146, 211, 169, 117, 153, 180, ]; -const VPK_PRIV_ACC_B: [u8; 33] = [ - 2, 79, 110, 46, 203, 29, 206, 205, 18, 86, 27, 189, 104, 103, 113, 181, 110, 53, 78, 172, 11, - 171, 190, 18, 126, 214, 81, 77, 192, 154, 58, 195, 238, +const VSK_Z_PRIV_ACC_B: [u8; 32] = [ + 165, 80, 169, 87, 248, 88, 167, 154, 27, 67, 131, 122, 50, 130, 111, 40, 164, 180, 204, 75, + 188, 140, 110, 132, 113, 133, 222, 8, 49, 123, 187, 18, ]; const NPK_PRIV_ACC_A: [u8; 32] = [ @@ -136,20 +136,20 @@ pub fn initial_priv_accounts_private_keys() -> Vec for WalletFfiError { + fn from(_value: Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + impl WalletFfiError { /// Check if it's [`WalletFfiError::Success`] or panic. pub fn unwrap(self) { diff --git a/lez/wallet-ffi/src/keys.rs b/lez/wallet-ffi/src/keys.rs index b473dba7..6a2c4d0b 100644 --- a/lez/wallet-ffi/src/keys.rs +++ b/lez/wallet-ffi/src/keys.rs @@ -1,6 +1,6 @@ //! Key retrieval functions. -use std::ptr; +use std::{ffi::CString, ptr}; use lee::{AccountId, PublicKey}; use wallet::AccountIdentity; @@ -127,7 +127,7 @@ pub unsafe extern "C" fn wallet_ffi_get_private_account_keys( // NPK is a 32-byte array let npk_bytes = key_chain.nullifier_public_key.0; - // VPK is a compressed secp256k1 point (33 bytes) + // VPK is an ML-KEM-768 encapsulation key (1184 bytes) let vpk_bytes = key_chain.viewing_public_key.to_bytes(); let vpk_len = vpk_bytes.len(); let vpk_vec = vpk_bytes.to_vec(); @@ -360,6 +360,7 @@ pub unsafe extern "C" fn wallet_ffi_free_account_identity( let FfiAccountIdentity { kind: _, account_id: _, + key_path, nullifier_secret_key: _, nullifier_public_key: _, viewing_public_key, @@ -374,6 +375,11 @@ pub unsafe extern "C" fn wallet_ffi_free_account_identity( ); drop(Box::from_raw(std::ptr::from_mut::<[u8]>(slice))); } + + if !key_path.is_null() { + let key_path_cstring = CString::from_raw(key_path); + drop(key_path_cstring); + } } } diff --git a/lez/wallet-ffi/src/transfer.rs b/lez/wallet-ffi/src/transfer.rs index 23dd3172..1fcd3133 100644 --- a/lez/wallet-ffi/src/transfer.rs +++ b/lez/wallet-ffi/src/transfer.rs @@ -1,11 +1,14 @@ //! Token transfer functions. -use std::{ffi::CString, ptr}; +use std::{ + ffi::{c_char, CStr, CString}, + ptr, +}; use lee::AccountId; use wallet::{ account::AccountIdWithPrivacy, cli::CliAccountMention, - program_facades::native_token_transfer::NativeTokenTransfer, + program_facades::native_token_transfer::NativeTokenTransfer, AccountIdentity, }; use crate::{ @@ -17,6 +20,14 @@ use crate::{ FfiPrivateAccountKeys, }; +fn optional_c_str(ptr: *const c_char) -> Option { + if ptr.is_null() { + return None; + } + let c_str = unsafe { CStr::from_ptr(ptr) }; + c_str.to_str().ok().map(str::to_owned) +} + /// Send a public token transfer. /// /// Transfers tokens from one public account to another on the network. @@ -75,15 +86,10 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( let transfer = NativeTokenTransfer(&wallet); - let from_mention = CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)); - let to_mention = CliAccountMention::Id(AccountIdWithPrivacy::Public(to_id)); - match block_on(transfer.send_public_transfer( - from_id, - to_id, + AccountIdentity::Public(from_id), + AccountIdentity::Public(to_id), amount, - &from_mention, - &to_mention, )) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) @@ -140,6 +146,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded( to_keys: *const FfiPrivateAccountKeys, to_identifier: *const FfiU128, amount: *const [u8; 16], + key_path: *const c_char, out_result: *mut FfiTransferResult, ) -> WalletFfiError { let wrapper = match get_wallet(handle) { @@ -176,11 +183,15 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded( }; let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data }); let amount = u128::from_le_bytes(unsafe { *amount }); + let from_mention = optional_c_str(key_path).map_or_else( + || CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)), + CliAccountMention::KeyPath, + ); let transfer = NativeTokenTransfer(&wallet); match block_on(transfer.send_shielded_transfer_to_outer_account( - from_id, + from_mention.into_public_identity(from_id), to_npk, to_vpk, to_identifier, @@ -262,7 +273,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded( let from_id = AccountId::new(unsafe { (*from).data }); let to_id = AccountId::new(unsafe { (*to).data }); let amount = u128::from_le_bytes(unsafe { *amount }); - let transfer = NativeTokenTransfer(&wallet); match block_on(transfer.send_deshielded_transfer(from_id, to_id, amount)) { @@ -357,7 +367,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private( }; let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data }); let amount = u128::from_le_bytes(unsafe { *amount }); - let transfer = NativeTokenTransfer(&wallet); match block_on(transfer.send_private_transfer_to_outer_account( @@ -423,6 +432,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned( from: *const FfiBytes32, to: *const FfiBytes32, amount: *const [u8; 16], + key_path: *const c_char, out_result: *mut FfiTransferResult, ) -> WalletFfiError { let wrapper = match get_wallet(handle) { @@ -446,10 +456,18 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned( let from_id = AccountId::new(unsafe { (*from).data }); let to_id = AccountId::new(unsafe { (*to).data }); let amount = u128::from_le_bytes(unsafe { *amount }); + let from_mention = optional_c_str(key_path).map_or_else( + || CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)), + CliAccountMention::KeyPath, + ); let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.send_shielded_transfer(from_id, to_id, amount)) { + match block_on(transfer.send_shielded_transfer( + from_mention.into_public_identity(from_id), + to_id, + amount, + )) { Ok((tx_hash, _shared_key)) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); @@ -529,7 +547,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private_owned( let from_id = AccountId::new(unsafe { (*from).data }); let to_id = AccountId::new(unsafe { (*to).data }); let amount = u128::from_le_bytes(unsafe { *amount }); - let transfer = NativeTokenTransfer(&wallet); match block_on(transfer.send_private_transfer_to_owned_account(from_id, to_id, amount)) { @@ -603,9 +620,7 @@ pub unsafe extern "C" fn wallet_ffi_register_public_account( let transfer = NativeTokenTransfer(&wallet); - let mention = CliAccountMention::Id(AccountIdWithPrivacy::Public(account_id)); - - match block_on(transfer.register_account(account_id, &mention)) { + match block_on(transfer.register_account(AccountIdentity::Public(account_id))) { Ok(tx_hash) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); @@ -673,7 +688,6 @@ pub unsafe extern "C" fn wallet_ffi_register_private_account( }; let account_id = AccountId::new(unsafe { (*account_id).data }); - let transfer = NativeTokenTransfer(&wallet); match block_on(transfer.register_account_private(account_id)) { diff --git a/lez/wallet-ffi/src/types.rs b/lez/wallet-ffi/src/types.rs index a0659d10..ad366b91 100644 --- a/lez/wallet-ffi/src/types.rs +++ b/lez/wallet-ffi/src/types.rs @@ -1,10 +1,14 @@ //! C-compatible type definitions for the FFI layer. use core::slice; -use std::{ffi::c_char, ptr}; +use std::{ + ffi::{c_char, CString}, + ptr, + str::FromStr as _, +}; use lee::{Data, SharedSecretKey}; -use lee_core::{encryption::shared_key_derivation::Secp256k1Point, NullifierPublicKey}; +use lee_core::{encryption::MlKem768EncapsulationKey, NullifierPublicKey}; use wallet::AccountIdentity; use crate::error::WalletFfiError; @@ -73,9 +77,9 @@ impl Default for FfiAccount { pub struct FfiPrivateAccountKeys { /// Nullifier public key (32 bytes). pub nullifier_public_key: FfiBytes32, - /// viewing public key (compressed secp256k1 point). + /// Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes). pub viewing_public_key: *const u8, - /// Length of viewing public key (typically 33 bytes). + /// Length of viewing public key (always 1184 bytes for ML-KEM-768). pub viewing_public_key_len: usize, } @@ -168,11 +172,14 @@ impl FfiPrivateAccountKeys { } pub fn vpk(&self) -> Result { - if self.viewing_public_key_len == 33 { + if self.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts(self.viewing_public_key, self.viewing_public_key_len) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok( + lee_core::encryption::ViewingPublicKey::from_bytes(slice.to_vec()) + .expect("wallet_ffi: length already validated to 1184 bytes"), + ) } else { Err(WalletFfiError::InvalidKeyValue) } @@ -185,12 +192,13 @@ impl FfiPrivateAccountKeys { pub enum FfiAccountIdentityKind { Public = 0, PublicNoSign = 1, - PrivateOwned = 2, - PrivateForeign = 3, - PrivatePdaOwned = 4, - PrivatePdaForeign = 5, - PrivateShared = 6, - PrivatePdaShared = 7, + PublicKeycard = 2, + PrivateOwned = 3, + PrivateForeign = 4, + PrivatePdaOwned = 5, + PrivatePdaForeign = 6, + PrivateShared = 7, + PrivatePdaShared = 8, } /// Struct representing an account identity, given to `AccountManager` at intialization. @@ -198,6 +206,8 @@ pub enum FfiAccountIdentityKind { pub struct FfiAccountIdentity { pub kind: FfiAccountIdentityKind, pub account_id: FfiBytes32, + /// C-compatible string. + pub key_path: *mut c_char, pub nullifier_secret_key: FfiBytes32, pub nullifier_public_key: FfiBytes32, pub viewing_public_key: *const u8, @@ -210,6 +220,7 @@ impl Default for FfiAccountIdentity { Self { kind: FfiAccountIdentityKind::Public, account_id: FfiBytes32::default(), + key_path: std::ptr::null_mut(), nullifier_secret_key: FfiBytes32::default(), nullifier_public_key: FfiBytes32::default(), viewing_public_key: std::ptr::null(), @@ -333,6 +344,17 @@ impl From for FfiAccountIdentity { account_id: account_id.into(), ..Default::default() }, + AccountIdentity::PublicKeycard { + account_id, + key_path, + } => Self { + kind: FfiAccountIdentityKind::PublicKeycard, + account_id: account_id.into(), + key_path: CString::into_raw( + CString::from_str(&key_path).expect("key_path should be a valid string"), + ), + ..Default::default() + }, AccountIdentity::PrivateOwned(account_id) => Self { kind: FfiAccountIdentityKind::PrivateOwned, account_id: account_id.into(), @@ -343,7 +365,7 @@ impl From for FfiAccountIdentity { vpk, identifier, } => { - let vpk_vec = vpk.0; + let vpk_vec = vpk.to_bytes().to_vec(); let vpk_len = vpk_vec.len(); let vpk_data = if vpk_len > 0 { let vpk_data_boxed = vpk_vec.into_boxed_slice(); @@ -372,7 +394,7 @@ impl From for FfiAccountIdentity { vpk, identifier, } => { - let vpk_vec = vpk.0; + let vpk_vec = vpk.to_bytes().to_vec(); let vpk_len = vpk_vec.len(); let vpk_data = if vpk_len > 0 { let vpk_data_boxed = vpk_vec.into_boxed_slice(); @@ -397,7 +419,7 @@ impl From for FfiAccountIdentity { vpk, identifier, } => { - let vpk_vec = vpk.0; + let vpk_vec = vpk.to_bytes().to_vec(); let vpk_len = vpk_vec.len(); let vpk_data = if vpk_len > 0 { let vpk_data_boxed = vpk_vec.into_boxed_slice(); @@ -423,7 +445,7 @@ impl From for FfiAccountIdentity { vpk, identifier, } => { - let vpk_vec = vpk.0; + let vpk_vec = vpk.to_bytes().to_vec(); let vpk_len = vpk_vec.len(); let vpk_data = if vpk_len > 0 { let vpk_data_boxed = vpk_vec.into_boxed_slice(); @@ -440,6 +462,7 @@ impl From for FfiAccountIdentity { viewing_public_key: vpk_data, viewing_public_key_len: vpk_len, identifier: identifier.into(), + ..Default::default() } } } @@ -449,20 +472,34 @@ impl From for FfiAccountIdentity { impl TryFrom<&FfiAccountIdentity> for AccountIdentity { type Error = WalletFfiError; + #[expect( + clippy::map_err_ignore, + reason = "`WalletFfiError` must be a trivial enum for FFI" + )] fn try_from(value: &FfiAccountIdentity) -> Result { match value.kind { FfiAccountIdentityKind::Public => Ok(Self::Public(value.account_id.into())), FfiAccountIdentityKind::PublicNoSign => Ok(Self::PublicNoSign(value.account_id.into())), + FfiAccountIdentityKind::PublicKeycard => { + let key_path = unsafe { CString::from_raw(value.key_path) } + .to_str()? + .to_owned(); + Ok(Self::PublicKeycard { + account_id: value.account_id.into(), + key_path, + }) + } FfiAccountIdentityKind::PrivateOwned => Ok(Self::PrivateOwned(value.account_id.into())), FfiAccountIdentityKind::PrivateForeign => { - let vpk = if value.viewing_public_key_len == 33 { + let vpk = if value.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts( value.viewing_public_key, value.viewing_public_key_len, ) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) } else { Err(WalletFfiError::InvalidKeyValue) }?; @@ -477,14 +514,15 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { Ok(Self::PrivatePdaOwned(value.account_id.into())) } FfiAccountIdentityKind::PrivatePdaForeign => { - let vpk = if value.viewing_public_key_len == 33 { + let vpk = if value.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts( value.viewing_public_key, value.viewing_public_key_len, ) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) } else { Err(WalletFfiError::InvalidKeyValue) }?; @@ -497,14 +535,15 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { }) } FfiAccountIdentityKind::PrivateShared => { - let vpk = if value.viewing_public_key_len == 33 { + let vpk = if value.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts( value.viewing_public_key, value.viewing_public_key_len, ) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) } else { Err(WalletFfiError::InvalidKeyValue) }?; @@ -517,14 +556,15 @@ impl TryFrom<&FfiAccountIdentity> for AccountIdentity { }) } FfiAccountIdentityKind::PrivatePdaShared => { - let vpk = if value.viewing_public_key_len == 33 { + let vpk = if value.viewing_public_key_len == 1184 { let slice = unsafe { slice::from_raw_parts( value.viewing_public_key, value.viewing_public_key_len, ) }; - Ok(Secp256k1Point(slice.to_vec())) + Ok(MlKem768EncapsulationKey::from_bytes(slice.to_vec()) + .map_err(|_| WalletFfiError::InvalidKeyValue)?) } else { Err(WalletFfiError::InvalidKeyValue) }?; @@ -556,7 +596,7 @@ mod tests { let pub_acc_id = (&public_key).into(); let nsk = [43; 32]; - let vpk = ViewingPublicKey::from_scalar([44; 32]); + let vpk = ViewingPublicKey::from_seed(&[44; 32], &[54; 32]); let npk = (&nsk).into(); let identifier = u128::from_le_bytes([45; 16]); @@ -573,6 +613,12 @@ mod tests { let acc_identity_1 = AccountIdentity::Public(pub_acc_id); let acc_identity_2 = AccountIdentity::PublicNoSign(pub_acc_id); + + let acc_identity_2_5 = AccountIdentity::PublicKeycard { + account_id: pub_acc_id, + key_path: "path/to/key".to_owned(), + }; + let acc_identity_3 = AccountIdentity::PrivateOwned(private_reg_acc_id); let acc_identity_4 = AccountIdentity::PrivateForeign { npk, @@ -602,6 +648,7 @@ mod tests { let ffi_acc_identity_1: FfiAccountIdentity = acc_identity_1.clone().into(); let ffi_acc_identity_2: FfiAccountIdentity = acc_identity_2.clone().into(); + let ffi_acc_identity_2_5: FfiAccountIdentity = acc_identity_2_5.clone().into(); let ffi_acc_identity_3: FfiAccountIdentity = acc_identity_3.clone().into(); let ffi_acc_identity_4: FfiAccountIdentity = acc_identity_4.clone().into(); let ffi_acc_identity_5: FfiAccountIdentity = acc_identity_5.clone().into(); @@ -614,6 +661,10 @@ mod tests { ffi_acc_identity_2.kind, FfiAccountIdentityKind::PublicNoSign ); + assert_eq!( + ffi_acc_identity_2_5.kind, + FfiAccountIdentityKind::PublicKeycard + ); assert_eq!( ffi_acc_identity_3.kind, FfiAccountIdentityKind::PrivateOwned @@ -641,6 +692,7 @@ mod tests { let acc_identity_res_1: AccountIdentity = (&ffi_acc_identity_1).try_into().unwrap(); let acc_identity_res_2: AccountIdentity = (&ffi_acc_identity_2).try_into().unwrap(); + let acc_identity_res_2_5: AccountIdentity = (&ffi_acc_identity_2_5).try_into().unwrap(); let acc_identity_res_3: AccountIdentity = (&ffi_acc_identity_3).try_into().unwrap(); let acc_identity_res_4: AccountIdentity = (&ffi_acc_identity_4).try_into().unwrap(); let acc_identity_res_5: AccountIdentity = (&ffi_acc_identity_5).try_into().unwrap(); @@ -650,6 +702,7 @@ mod tests { assert_eq!(acc_identity_res_1, acc_identity_1); assert_eq!(acc_identity_res_2, acc_identity_2); + assert_eq!(acc_identity_res_2_5, acc_identity_2_5); assert_eq!(acc_identity_res_3, acc_identity_3); assert_eq!(acc_identity_res_4, acc_identity_4); assert_eq!(acc_identity_res_5, acc_identity_5); diff --git a/lez/wallet-ffi/wallet_ffi.h b/lez/wallet-ffi/wallet_ffi.h index 0c2f14df..a8506a8a 100644 --- a/lez/wallet-ffi/wallet_ffi.h +++ b/lez/wallet-ffi/wallet_ffi.h @@ -119,12 +119,13 @@ typedef enum WalletFfiError { typedef enum FfiAccountIdentityKind { PUBLIC = 0, PUBLIC_NO_SIGN = 1, - PRIVATE_OWNED = 2, - PRIVATE_FOREIGN = 3, - PRIVATE_PDA_OWNED = 4, - PRIVATE_PDA_FOREIGN = 5, - PRIVATE_SHARED = 6, - PRIVATE_PDA_SHARED = 7, + PUBLIC_KEYCARD = 2, + PRIVATE_OWNED = 3, + PRIVATE_FOREIGN = 4, + PRIVATE_PDA_OWNED = 5, + PRIVATE_PDA_FOREIGN = 6, + PRIVATE_SHARED = 7, + PRIVATE_PDA_SHARED = 8, } FfiAccountIdentityKind; /** @@ -153,11 +154,11 @@ typedef struct FfiPrivateAccountKeys { */ struct FfiBytes32 nullifier_public_key; /** - * viewing public key (compressed secp256k1 point). + * Viewing public key (ML-KEM-768 encapsulation key, 1184 bytes). */ const uint8_t *viewing_public_key; /** - * Length of viewing public key (typically 33 bytes). + * Length of viewing public key (always 1184 bytes for ML-KEM-768). */ uintptr_t viewing_public_key_len; } FfiPrivateAccountKeys; @@ -230,6 +231,10 @@ typedef struct FfiInstructionWords { typedef struct FfiAccountIdentity { enum FfiAccountIdentityKind kind; struct FfiBytes32 account_id; + /** + * C-compatible string. + */ + char *key_path; struct FfiBytes32 nullifier_secret_key; struct FfiBytes32 nullifier_public_key; const uint8_t *viewing_public_key; @@ -997,6 +1002,7 @@ enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle, const struct FfiPrivateAccountKeys *to_keys, const struct FfiU128 *to_identifier, const uint8_t (*amount)[16], + const char *key_path, struct FfiTransferResult *out_result); /** @@ -1104,6 +1110,7 @@ enum WalletFfiError wallet_ffi_transfer_shielded_owned(struct WalletHandle *hand const struct FfiBytes32 *from, const struct FfiBytes32 *to, const uint8_t (*amount)[16], + const char *key_path, struct FfiTransferResult *out_result); /** diff --git a/lez/wallet/Cargo.toml b/lez/wallet/Cargo.toml index 7044f557..40222715 100644 --- a/lez/wallet/Cargo.toml +++ b/lez/wallet/Cargo.toml @@ -21,7 +21,7 @@ ata_core.workspace = true bip39.workspace = true pyo3.workspace = true rpassword = "7" -zeroize = "1" +zeroize.workspace = true keycard_wallet.workspace = true anyhow.workspace = true @@ -46,6 +46,11 @@ optfield = "0.4.0" url.workspace = true derive_more = { workspace = true, features = ["display"] } +[features] +# Enables `keycard get-private-keys` command that prints sensitive secret keys to terminal. +# Never enable in production builds. +keycard-debug = [] + [dev-dependencies] tempfile.workspace = true key_protocol = { workspace = true, features = ["test_utils"] } diff --git a/lez/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index a09a2a9b..ce9d1833 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -2,12 +2,13 @@ use core::fmt; use anyhow::Result; use key_protocol::key_management::ephemeral_key_holder::EphemeralKeyHolder; -use lee::{AccountId, PrivateKey}; +use keycard_wallet::{KeycardWallet, python_path}; +use lee::{AccountId, PrivateKey, PublicKey, Signature}; use lee_core::{ Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey}, }; use crate::{ExecutionFailureKind, WalletCore}; @@ -17,6 +18,11 @@ pub enum AccountIdentity { Public(AccountId), /// A public account without signing. Would not try to sign, even if account is owned. PublicNoSign(AccountId), + /// A public account from keycard. Mandatory signing. + PublicKeycard { + account_id: AccountId, + key_path: String, + }, PrivateOwned(AccountId), PrivateForeign { npk: NullifierPublicKey, @@ -59,6 +65,14 @@ impl fmt::Debug for AccountIdentity { match self { Self::Public(id) => f.debug_tuple("Public").field(id).finish(), Self::PublicNoSign(id) => f.debug_tuple("PublicNoSign").field(id).finish(), + Self::PublicKeycard { + account_id, + key_path: _, + } => f + .debug_struct("PublicKeycard") + .field("account_id", account_id) + .field("key_path", &"") + .finish(), Self::PrivateOwned(id) => f.debug_tuple("PrivateOwned").field(id).finish(), Self::PrivateForeign { npk, @@ -118,7 +132,26 @@ impl AccountIdentity { /// Note: `PublicNoSign` still counts as public, the variant just suppresses the signing-key /// lookup. pub const fn is_public(&self) -> bool { - matches!(&self, Self::Public(_) | Self::PublicNoSign(_)) + matches!( + &self, + Self::Public(_) | Self::PublicNoSign(_) | Self::PublicKeycard { .. } + ) + } + + /// Returns the `AccountId` for public variants. Used by facades that need the raw ID + /// for derived-address computation alongside the identity. + #[must_use] + pub const fn public_account_id(&self) -> Option { + match self { + Self::Public(id) | Self::PublicNoSign(id) => Some(*id), + Self::PublicKeycard { account_id, .. } => Some(*account_id), + Self::PrivateOwned(_) + | Self::PrivateForeign { .. } + | Self::PrivatePdaOwned(_) + | Self::PrivatePdaForeign { .. } + | Self::PrivateShared { .. } + | Self::PrivatePdaShared { .. } => None, + } } #[must_use] @@ -136,10 +169,7 @@ impl AccountIdentity { } pub struct PrivateAccountKeys { - pub npk: NullifierPublicKey, pub ssk: SharedSecretKey, - pub vpk: ViewingPublicKey, - pub epk: EphemeralPublicKey, } enum State { @@ -147,11 +177,16 @@ enum State { account: AccountWithMetadata, sk: Option, }, + PublicKeycard { + account: AccountWithMetadata, + key_path: String, + }, Private(AccountPreparedData), } pub struct AccountManager { states: Vec, + pin: Option, } impl AccountManager { @@ -160,6 +195,7 @@ impl AccountManager { accounts: Vec, ) -> Result { let mut states = Vec::with_capacity(accounts.len()); + let mut pin = None; for account in accounts { let state = match account { @@ -185,6 +221,35 @@ impl AccountManager { State::Public { account, sk } } + AccountIdentity::PublicKeycard { + account_id, + key_path, + } => { + let acc = wallet + .get_account_public(account_id) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + + let account = AccountWithMetadata::new(acc.clone(), true, account_id); + + if pin.is_none() { + pin = Some( + crate::helperfunctions::read_pin() + .map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< + pyo3::exceptions::PyRuntimeError, + _, + >( + e.to_string() + )) + })? + .as_str() + .to_owned(), + ); + } + + State::PublicKeycard { account, key_path } + } AccountIdentity::PrivateOwned(account_id) => { let pre = private_key_tree_acc_preparation(wallet, account_id, false).await?; @@ -197,9 +262,9 @@ impl AccountManager { } => { let acc = lee_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 eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -226,9 +291,9 @@ impl AccountManager { } => { let acc = lee_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 eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); let pre = AccountPreparedData { nsk: None, npk, @@ -275,40 +340,41 @@ impl AccountManager { states.push(state); } - Ok(Self { states }) + Ok(Self { states, pin }) } pub fn pre_states(&self) -> Vec { self.states .iter() .map(|state| match state { - State::Public { account, .. } => account.clone(), + State::Public { account, .. } | State::PublicKeycard { account, .. } => { + account.clone() + } State::Private(pre) => pre.pre_state.clone(), }) .collect() } pub fn public_account_nonces(&self) -> Vec { - self.states - .iter() - .filter_map(|state| match state { - State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), - State::Private(_) => None, - }) - .collect() + // Must match the signature order produced by sign_message(): local accounts first, + // keycard accounts second. + let local = self.states.iter().filter_map(|state| match state { + State::Public { account, sk } => sk.as_ref().map(|_| account.account.nonce), + State::PublicKeycard { .. } | State::Private(_) => None, + }); + let keycard = self.states.iter().filter_map(|state| match state { + State::PublicKeycard { account, .. } => Some(account.account.nonce), + State::Public { .. } | State::Private(_) => None, + }); + local.chain(keycard).collect() } pub fn private_account_keys(&self) -> Vec { self.states .iter() .filter_map(|state| match state { - State::Private(pre) => Some(PrivateAccountKeys { - npk: pre.npk, - ssk: pre.ssk, - vpk: pre.vpk.clone(), - epk: pre.epk.clone(), - }), - State::Public { .. } => None, + State::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }), + State::Public { .. } | State::PublicKeycard { .. } => None, }) .collect() } @@ -321,9 +387,11 @@ impl AccountManager { self.states .iter() .map(|state| match state { - State::Public { .. } => InputAccountIdentity::Public, + State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public, State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) { (Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate { + epk: pre.epk.clone(), + view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), ssk: pre.ssk, nsk, membership_proof, @@ -331,6 +399,8 @@ impl AccountManager { seed: None, }, _ => InputAccountIdentity::PrivatePdaInit { + epk: pre.epk.clone(), + view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, @@ -340,6 +410,8 @@ impl AccountManager { State::Private(pre) => match (pre.nsk, pre.proof.clone()) { (Some(nsk), Some(membership_proof)) => { InputAccountIdentity::PrivateAuthorizedUpdate { + epk: pre.epk.clone(), + view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), ssk: pre.ssk, nsk, membership_proof, @@ -347,11 +419,15 @@ impl AccountManager { } } (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { + epk: pre.epk.clone(), + view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), ssk: pre.ssk, nsk, identifier: pre.identifier, }, (None, _) => InputAccountIdentity::PrivateUnauthorized { + epk: pre.epk.clone(), + view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk), npk: pre.npk, ssk: pre.ssk, identifier: pre.identifier, @@ -365,21 +441,61 @@ impl AccountManager { self.states .iter() .filter_map(|state| match state { - State::Public { account, .. } => Some(account.account_id), + State::Public { account, .. } | State::PublicKeycard { account, .. } => { + Some(account.account_id) + } State::Private(_) => None, }) .collect() } - pub fn public_account_auth(&self) -> Vec<&PrivateKey> { + pub fn public_non_keycard_account_auth(&self) -> Vec<&PrivateKey> { self.states .iter() .filter_map(|state| match state { State::Public { sk, .. } => sk.as_ref(), - State::Private(_) => None, + State::PublicKeycard { .. } | State::Private(_) => None, }) .collect() } + + pub fn sign_message(&self, message_hash: [u8; 32]) -> Result> { + let mut sigs: Vec<(Signature, PublicKey)> = self + .public_non_keycard_account_auth() + .into_iter() + .map(|key| { + ( + Signature::new(key, &message_hash), + PublicKey::new_from_private_key(key), + ) + }) + .collect(); + + let keycard_paths: Vec<&str> = self + .states + .iter() + .filter_map(|state| match state { + State::PublicKeycard { key_path, .. } => Some(key_path.as_str()), + State::Private(_) | State::Public { .. } => None, + }) + .collect(); + + if let Some(pin) = self.pin.clone() { + pyo3::Python::attach(|py| -> pyo3::PyResult<()> { + python_path::add_python_path(py)?; + let wallet = KeycardWallet::new(py)?; + wallet.connect(py, &pin)?; + for path in keycard_paths { + sigs.push(wallet.sign_message_for_path(py, path, &message_hash)?); + } + let _res = wallet.close_session(py); + Ok(()) + }) + .map_err(anyhow::Error::from)?; + } + + Ok(sigs) + } } struct AccountPreparedData { @@ -426,9 +542,9 @@ async fn private_key_tree_acc_preparation( // support from that in the wallet. let sender_pre = AccountWithMetadata::new(from_acc.account.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(); + let eph_holder = EphemeralKeyHolder::new(&from_vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); Ok(AccountPreparedData { nsk: Some(nsk), @@ -466,9 +582,10 @@ async fn private_shared_acc_preparation( .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(); + let eph_holder = EphemeralKeyHolder::new(&vpk); + let ssk = eph_holder.calculate_shared_secret_sender(); + let epk = eph_holder.ephemeral_public_key().clone(); + Ok(AccountPreparedData { nsk: Some(nsk), npk, @@ -491,7 +608,7 @@ mod tests { let acc = AccountIdentity::PrivateShared { nsk: [0; 32], npk: NullifierPublicKey([1; 32]), - vpk: ViewingPublicKey::from_scalar([2; 32]), + vpk: ViewingPublicKey::from_seed(&[2_u8; 32], &[3_u8; 32]), identifier: 42, }; assert!(acc.is_private()); diff --git a/lez/wallet/src/cli/account.rs b/lez/wallet/src/cli/account.rs index 53c55537..e0ec2d0f 100644 --- a/lez/wallet/src/cli/account.rs +++ b/lez/wallet/src/cli/account.rs @@ -15,6 +15,12 @@ use crate::{ /// Represents generic chain CLI subcommand. #[derive(Subcommand, Debug, Clone)] pub enum AccountSubcommand { + /// Resolve an account mention and print just the account ID (no privacy prefix). + Id { + /// Account id with privacy prefix, label, or BIP-32 key path. + #[arg(long)] + account_id: CliAccountMention, + }, /// Get account data. Get { /// Flag to get raw account data. @@ -51,6 +57,20 @@ pub enum AccountSubcommand { /// Import external account. #[command(subcommand)] Import(ImportSubcommand), + /// Print the npk and vpk for a private account, one per line. + /// + /// Outputs two lines: npk (hex) then vpk (hex). Save to a file and share it + /// with senders so they can reference it with `--to-keys /path/to/file`. + /// + /// ```text + /// wallet account show-keys --account-id Private/... > alice.keys + /// ``` + #[command(name = "show-keys")] + ShowKeys { + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(long)] + account_id: CliAccountMention, + }, } /// Represents generic register CLI subcommand. @@ -225,7 +245,7 @@ impl WalletSubcommand for NewSubcommand { 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)); + println!("VPK: {}", hex::encode(info.vpk.to_bytes())); wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::RegisterAccount { @@ -261,6 +281,14 @@ impl WalletSubcommand for AccountSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { + Self::Id { account_id } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let id = match resolved { + AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id, + }; + println!("{id}"); + Ok(SubcommandReturnValue::Empty) + } Self::Get { raw, keys, @@ -419,6 +447,25 @@ impl WalletSubcommand for AccountSubcommand { Self::Import(import_subcommand) => { import_subcommand.handle_subcommand(wallet_core).await } + Self::ShowKeys { account_id } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Private(account_id) = resolved else { + anyhow::bail!( + "wallet::cli::account::AccountSubcommand::ShowKeys: show-keys is only available for private accounts" + ); + }; + let entry = wallet_core + .storage() + .key_chain() + .private_account(account_id) + .ok_or_else(|| anyhow::anyhow!("wallet::cli::account::AccountSubcommand::ShowKeys: private account not found in wallet"))?; + println!("{}", hex::encode(entry.key_chain.nullifier_public_key.0)); + println!( + "{}", + hex::encode(entry.key_chain.viewing_public_key.to_bytes()) + ); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/lez/wallet/src/cli/group.rs b/lez/wallet/src/cli/group.rs index 3f1f5c67..a4bb12c9 100644 --- a/lez/wallet/src/cli/group.rs +++ b/lez/wallet/src/cli/group.rs @@ -1,6 +1,9 @@ use anyhow::{Context as _, Result}; use clap::Subcommand; -use key_protocol::key_management::group_key_holder::{GroupKeyHolder, SealingPublicKey}; +use key_protocol::key_management::{ + group_key_holder::{GroupKeyHolder, SealingPublicKey}, + secret_holders::ViewingSecretKey, +}; use crate::{ WalletCore, @@ -149,9 +152,15 @@ impl WalletSubcommand for GroupSubcommand { anyhow::bail!("Sealing key already exists. Each wallet has one sealing key."); } - let mut secret: lee_core::encryption::Scalar = [0_u8; 32]; - rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut secret); - let public_key = SealingPublicKey::from_scalar(secret); + let mut d = [0_u8; 32]; + let mut r = [0_u8; 32]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut d); + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut r); + let secret = ViewingSecretKey::new(d, r); + let ek_bytes = lee_core::encryption::ViewingPublicKey::from_seed(&d, &r) + .to_bytes() + .to_vec(); + let public_key = SealingPublicKey::from_bytes(ek_bytes); wallet_core.set_sealing_secret_key(secret); wallet_core.store_persistent_data()?; diff --git a/lez/wallet/src/cli/keycard.rs b/lez/wallet/src/cli/keycard.rs index ead1e84b..a2eae73c 100644 --- a/lez/wallet/src/cli/keycard.rs +++ b/lez/wallet/src/cli/keycard.rs @@ -16,6 +16,20 @@ pub enum KeycardSubcommand { Disconnect, Init, Load, + /// Retrieve the private keys (NSK, VSK) for a given BIP-32 key path. + /// + /// Prints raw key material to stdout — intended for debugging only. + /// Requires --reveal to confirm intent. + /// Only available when built with the `keycard-debug` feature. + #[cfg(feature = "keycard-debug")] + GetPrivateKeys { + /// BIP-32 derivation path, e.g. `m/44'/60'/0'/0/0`. + #[arg(long)] + key_path: String, + /// Confirm that raw NSK and VSK should be disclosed on stdout. + #[arg(long)] + reveal: bool, + }, } impl WalletSubcommand for KeycardSubcommand { @@ -25,8 +39,9 @@ impl WalletSubcommand for KeycardSubcommand { ) -> Result { match self { Self::Available => { - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + Python::attach(|py| { + python_path::add_python_path(py) + .expect("`wallet::keycard::available`: unable to setup python path"); let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::available`: invalid data received for pin"); @@ -46,8 +61,9 @@ impl WalletSubcommand for KeycardSubcommand { Self::Connect => { let pin = read_pin()?; - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + Python::attach(|py| { + python_path::add_python_path(py) + .expect("`wallet::keycard::connect`: unable to setup python path"); let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::connect`: invalid keycard wallet provided"); @@ -65,8 +81,9 @@ impl WalletSubcommand for KeycardSubcommand { Self::Disconnect => { let pin = read_pin()?; - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + Python::attach(|py| { + python_path::add_python_path(py) + .expect("`wallet::keycard::disconnect`: unable to setup python path"); let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::disconnect`: invalid keycard wallet provided"); @@ -88,8 +105,9 @@ impl WalletSubcommand for KeycardSubcommand { Self::Init => { let pin = read_pin()?; - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + Python::attach(|py| { + python_path::add_python_path(py) + .expect("`wallet::keycard::init`: unable to setup python path"); let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::init`: invalid keycard wallet provided"); @@ -110,8 +128,9 @@ impl WalletSubcommand for KeycardSubcommand { let pin = read_pin()?; let mnemonic = read_mnemonic()?; - Python::with_gil(|py| { - python_path::add_python_path(py).expect("keycard_wallet.py not found"); + Python::attach(|py| { + python_path::add_python_path(py) + .expect("`wallet::keycard::load`: unable to setup python path"); let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::load`: invalid keycard wallet provided"); @@ -131,6 +150,27 @@ impl WalletSubcommand for KeycardSubcommand { Ok(SubcommandReturnValue::Empty) } + #[cfg(feature = "keycard-debug")] + Self::GetPrivateKeys { key_path, reveal } => { + if !reveal { + eprintln!( + "WARNING: pass --reveal to print NSK and VSK. \ + Disclosing either key fully compromises the account's privacy." + ); + return Ok(SubcommandReturnValue::Empty); + } + eprintln!( + "WARNING: NSK and VSK are being printed to stdout. \ + Any terminal log, scrollback, or screen recording captures these keys." + ); + let pin = read_pin()?; + let (nsk, vsk) = + KeycardWallet::get_private_keys_for_path_with_connect(&pin, &key_path) + .map_err(anyhow::Error::from)?; + println!("NSK: {}", hex::encode(*nsk)); + println!("VSK: {}", hex::encode(*vsk)); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index c6e5ef3e..57c40b95 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -138,13 +138,34 @@ impl CliAccountMention { Self::KeyPath(path) => { let pin = read_pin()?; let id_str = - keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path) - .map_err(anyhow::Error::from)?; + keycard_wallet::KeycardWallet::get_public_account_id_for_path_with_connect( + &pin, path, + ) + .map_err(anyhow::Error::from)?; AccountIdWithPrivacy::from_str(&id_str) .map_err(|e| anyhow::anyhow!("Invalid account id from keycard: {e}")) } } } + + #[must_use] + pub fn key_path(&self) -> Option<&str> { + match self { + Self::KeyPath(path) => Some(path), + Self::Id(_) | Self::Label(_) => None, + } + } + + #[must_use] + pub fn into_public_identity(self, account_id: lee::AccountId) -> crate::AccountIdentity { + match self { + Self::KeyPath(key_path) => crate::AccountIdentity::PublicKeycard { + account_id, + key_path, + }, + Self::Id(_) | Self::Label(_) => crate::AccountIdentity::Public(account_id), + } + } } impl FromStr for CliAccountMention { @@ -285,6 +306,31 @@ pub fn read_password_from_stdin() -> Result { Ok(password.trim().to_owned()) } +/// Parse a keys file exported by `wallet account show-keys`. +/// +/// The file format is two lines: +/// - Line 1: npk as hex (64 chars, 32 bytes). +/// - Line 2: vpk as hex (2368 chars, 1184 bytes). +/// +/// Returns `(npk_bytes, vpk_bytes)`. +pub fn read_keys_file(path: &str) -> Result<(Vec, Vec)> { + let content = std::fs::read_to_string(path).with_context(|| { + format!("wallet::cli::read_keys_file: failed to read keys file: {path}") + })?; + let mut lines = content.lines().filter(|l| !l.trim().is_empty()); + let npk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing npk (line 1)") + })?; + let vpk_hex = lines.next().ok_or_else(|| { + anyhow::anyhow!("wallet::cli::read_keys_file: keys file is missing vpk (line 2)") + })?; + let npk = hex::decode(npk_hex.trim()) + .context("wallet::cli::read_keys_file: npk in keys file must be valid hex")?; + let vpk = hex::decode(vpk_hex.trim()) + .context("wallet::cli::read_keys_file: vpk in keys file must be valid hex")?; + Ok((npk, vpk)) +} + pub fn read_mnemonic_from_stdin() -> Result { let mut phrase = String::new(); @@ -328,3 +374,77 @@ pub async fn execute_keys_restoration(wallet_core: &mut WalletCore, depth: u32) Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn read_keys_file_roundtrip() { + let npk = [0xab_u8; 32]; + let vpk = [0xcd_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test.keys"); + + // Simulate what `wallet account show-keys` writes. + std::fs::write( + &path, + format!("{}\n{}\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + + assert_eq!(parsed_npk, npk, "npk must round-trip through the keys file"); + assert_eq!( + parsed_vpk, + vpk.to_vec(), + "vpk must round-trip through the keys file" + ); + } + + #[test] + fn read_keys_file_missing_vpk_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("incomplete.keys"); + std::fs::write(&path, format!("{}\n", hex::encode([0xab_u8; 32]))).unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "missing vpk line must return an error"); + assert!( + result.unwrap_err().to_string().contains("missing vpk"), + "error must mention missing vpk" + ); + } + + #[test] + fn read_keys_file_invalid_hex_returns_error() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("badhex.keys"); + std::fs::write(&path, "not-hex\nalso-not-hex\n").unwrap(); + + let result = read_keys_file(path.to_str().unwrap()); + assert!(result.is_err(), "invalid hex must return an error"); + } + + #[test] + fn read_keys_file_ignores_blank_lines() { + let npk = [0x11_u8; 32]; + let vpk = [0x22_u8; 1184]; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("blanks.keys"); + + // Extra blank lines around the data should be tolerated. + std::fs::write( + &path, + format!("\n{}\n\n{}\n\n", hex::encode(npk), hex::encode(vpk)), + ) + .unwrap(); + + let (parsed_npk, parsed_vpk) = read_keys_file(path.to_str().unwrap()).unwrap(); + assert_eq!(parsed_npk, npk); + assert_eq!(parsed_vpk, vpk.to_vec()); + } +} diff --git a/lez/wallet/src/cli/programs/amm.rs b/lez/wallet/src/cli/programs/amm.rs index 532458de..6cdeb07c 100644 --- a/lez/wallet/src/cli/programs/amm.rs +++ b/lez/wallet/src/cli/programs/amm.rs @@ -131,25 +131,28 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { balance_a, balance_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { - Amm(wallet_core) + let tx_hash = Amm(wallet_core) .send_new_definition( - user_holding_a, - user_holding_b, - user_holding_lp, + user_holding_a.into_public_identity(a), + user_holding_b.into_public_identity(b), + user_holding_lp.into_public_identity(lp), balance_a, balance_b, ) .await?; - + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } _ => { @@ -165,23 +168,23 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { min_amount_out, token_definition, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b) { - ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - ) => { - Amm(wallet_core) + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + match (a_id, b_id) { + (AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => { + let tx_hash = Amm(wallet_core) .send_swap_exact_input( - user_holding_a, - user_holding_b, + user_holding_a.into_public_identity(a), + user_holding_b.into_public_identity(b), amount_in, min_amount_out, token_definition, ) .await?; - + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } _ => { @@ -197,23 +200,23 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { max_amount_in, token_definition, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b) { - ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - ) => { - Amm(wallet_core) + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + match (a_id, b_id) { + (AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => { + let tx_hash = Amm(wallet_core) .send_swap_exact_output( - user_holding_a, - user_holding_b, + user_holding_a.into_public_identity(a), + user_holding_b.into_public_identity(b), exact_amount_out, max_amount_in, token_definition, ) .await?; - + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } _ => { @@ -230,26 +233,29 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { max_amount_a, max_amount_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { - Amm(wallet_core) + let tx_hash = Amm(wallet_core) .send_add_liquidity( - user_holding_a, - user_holding_b, - user_holding_lp, + user_holding_a.into_public_identity(a), + user_holding_b.into_public_identity(b), + user_holding_lp.into_public_identity(lp), min_amount_lp, max_amount_a, max_amount_b, ) .await?; - + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } _ => { @@ -266,26 +272,29 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { min_amount_a, min_amount_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { - Amm(wallet_core) + let tx_hash = Amm(wallet_core) .send_remove_liquidity( - user_holding_a, - user_holding_b, - user_holding_lp, + a, + b, + user_holding_lp.into_public_identity(lp), balance_lp, min_amount_a, min_amount_b, ) .await?; - + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } _ => { diff --git a/lez/wallet/src/cli/programs/ata.rs b/lez/wallet/src/cli/programs/ata.rs index dab67cc6..b77ff61f 100644 --- a/lez/wallet/src/cli/programs/ata.rs +++ b/lez/wallet/src/cli/programs/ata.rs @@ -91,14 +91,18 @@ impl WalletSubcommand for AtaSubcommand { owner, token_definition, } => { - let owner = owner.resolve(wallet_core.storage())?; + let owner_resolved = owner.resolve(wallet_core.storage())?; let definition_id = token_definition; - match owner { + match owner_resolved { AccountIdWithPrivacy::Public(owner_id) => { - Ata(wallet_core) - .send_create(owner_id, definition_id) + let tx_hash = Ata(wallet_core) + .send_create(owner.into_public_identity(owner_id), definition_id) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } AccountIdWithPrivacy::Private(owner_id) => { @@ -127,15 +131,24 @@ impl WalletSubcommand for AtaSubcommand { to, amount, } => { - let from = from.resolve(wallet_core.storage())?; + let from_resolved = from.resolve(wallet_core.storage())?; let definition_id = token_definition; let to_id = to; - match from { + match from_resolved { AccountIdWithPrivacy::Public(from_id) => { - Ata(wallet_core) - .send_transfer(from_id, definition_id, to_id, amount) + let tx_hash = Ata(wallet_core) + .send_transfer( + from.into_public_identity(from_id), + definition_id, + to_id, + amount, + ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } AccountIdWithPrivacy::Private(from_id) => { @@ -163,14 +176,22 @@ impl WalletSubcommand for AtaSubcommand { token_definition, amount, } => { - let holder = holder.resolve(wallet_core.storage())?; + let holder_resolved = holder.resolve(wallet_core.storage())?; let definition_id = token_definition; - match holder { + match holder_resolved { AccountIdWithPrivacy::Public(holder_id) => { - Ata(wallet_core) - .send_burn(holder_id, definition_id, amount) + let tx_hash = Ata(wallet_core) + .send_burn( + holder.into_public_identity(holder_id), + definition_id, + amount, + ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } AccountIdWithPrivacy::Private(holder_id) => { diff --git a/lez/wallet/src/cli/programs/native_token_transfer.rs b/lez/wallet/src/cli/programs/native_token_transfer.rs index 144d7603..eba1da0c 100644 --- a/lez/wallet/src/cli/programs/native_token_transfer.rs +++ b/lez/wallet/src/cli/programs/native_token_transfer.rs @@ -1,11 +1,11 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; use crate::{ AccDecodeData::Decode, - WalletCore, + AccountIdentity, WalletCore, account::AccountIdWithPrivacy, cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand}, program_facades::native_token_transfer::NativeTokenTransfer, @@ -34,13 +34,17 @@ pub enum AuthTransferSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -60,7 +64,7 @@ impl WalletSubcommand for AuthTransferSubcommand { match resolved { AccountIdWithPrivacy::Public(pub_account_id) => { let tx_hash = NativeTokenTransfer(wallet_core) - .register_account(pub_account_id, &account_id) + .register_account(account_id.into_public_identity(pub_account_id)) .await?; println!("Transaction hash is {tx_hash}"); @@ -100,9 +104,18 @@ impl WalletSubcommand for AuthTransferSubcommand { to: to_account, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + // Resolve --to-keys into --to-npk / --to-vpk equivalents. + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from_account.resolve(wallet_core.storage())?; let to = to_account .as_ref() @@ -124,12 +137,11 @@ impl WalletSubcommand for AuthTransferSubcommand { } (Some(to), None, None) => match (from, to) { (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => { + let to_mention = to_account.expect("matched Some branch"); NativeTokenTransferProgramSubcommand::Public { - from, - to, + from: Some(from_account.into_public_identity(from)), + to: Some(to_mention.into_public_identity(to)), amount, - from_mention: from_account, - to_mention: to_account.expect("matched Some branch"), } } ( @@ -148,7 +160,7 @@ impl WalletSubcommand for AuthTransferSubcommand { (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => { NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedOwned { - from, + from: Some(from_account.into_public_identity(from)), to, amount, }, @@ -170,7 +182,7 @@ impl WalletSubcommand for AuthTransferSubcommand { AccountIdWithPrivacy::Public(from) => { NativeTokenTransferProgramSubcommand::Shielded( NativeTokenTransferProgramSubcommandShielded::ShieldedForeign { - from, + from: Some(from_account.into_public_identity(from)), to_npk, to_vpk, to_identifier, @@ -194,19 +206,13 @@ pub enum NativeTokenTransferProgramSubcommand { /// /// Public operation. Public { - /// from - valid 32 byte hex string. - #[arg(long)] - from: AccountId, - /// to - valid 32 byte hex string. - #[arg(long)] - to: AccountId, + #[arg(skip)] + from: Option, + #[arg(skip)] + to: Option, /// amount - amount of balance to move. #[arg(long)] amount: u128, - #[arg(skip)] - from_mention: CliAccountMention, - #[arg(skip)] - to_mention: CliAccountMention, }, /// Private execution. #[command(subcommand)] @@ -239,8 +245,8 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// Shielded operation. ShieldedOwned { /// from - valid 32 byte hex string. - #[arg(long)] - from: AccountId, + #[arg(skip)] + from: Option, /// to - valid 32 byte hex string. #[arg(long)] to: AccountId, @@ -252,13 +258,12 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// /// Shielded operation. ShieldedForeign { - /// from - valid 32 byte hex string. - #[arg(long)] - from: AccountId, + #[arg(skip)] + from: Option, /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -298,7 +303,7 @@ pub enum NativeTokenTransferProgramSubcommandPrivate { /// `to_npk` - valid 32 byte hex string. #[arg(long)] to_npk: String, - /// `to_vpk` - valid 33 byte hex string. + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] to_vpk: String, /// Identifier for the recipient's private account. @@ -350,11 +355,10 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - lee_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; + let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_from, _]) = NativeTokenTransfer(wallet_core) .send_private_transfer_to_outer_account( @@ -395,7 +399,11 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { match self { Self::ShieldedOwned { from, to, amount } => { let (tx_hash, secret) = NativeTokenTransfer(wallet_core) - .send_shielded_transfer(from, to, amount) + .send_shielded_transfer( + from.expect("from set during Send dispatch"), + to, + amount, + ) .await?; println!("Transaction hash is {tx_hash}"); @@ -427,15 +435,14 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_npk.copy_from_slice(&to_npk_res); let to_npk = lee_core::NullifierPublicKey(to_npk); - let to_vpk_res = hex::decode(to_vpk)?; - let mut to_vpk = [0_u8; 33]; - to_vpk.copy_from_slice(&to_vpk_res); - let to_vpk = - lee_core::encryption::shared_key_derivation::Secp256k1Point(to_vpk.to_vec()); + let to_vpk_res = hex::decode(&to_vpk) + .context("wallet::cli::programs::native_token_transfer: to_vpk must be a valid hex string")?; + let to_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(to_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = NativeTokenTransfer(wallet_core) .send_shielded_transfer_to_outer_account( - from, + from.expect("from set during Send dispatch"), to_npk, to_vpk, to_identifier.unwrap_or_else(rand::random), @@ -487,15 +494,13 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) } - Self::Public { - from, - to, - amount, - from_mention, - to_mention, - } => { + Self::Public { from, to, amount } => { let tx_hash = NativeTokenTransfer(wallet_core) - .send_public_transfer(from, to, amount, &from_mention, &to_mention) + .send_public_transfer( + from.expect("from is set during Send dispatch"), + to.expect("to is set during Send dispatch"), + amount, + ) .await?; println!("Transaction hash is {tx_hash}"); diff --git a/lez/wallet/src/cli/programs/token.rs b/lez/wallet/src/cli/programs/token.rs index 45e73623..4a64f902 100644 --- a/lez/wallet/src/cli/programs/token.rs +++ b/lez/wallet/src/cli/programs/token.rs @@ -1,11 +1,11 @@ -use anyhow::Result; +use anyhow::{Context as _, Result}; use clap::Subcommand; use common::transaction::LeeTransaction; use lee::AccountId; use crate::{ AccDecodeData::Decode, - WalletCore, + AccountIdentity, WalletCore, account::AccountIdWithPrivacy, cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand}, program_facades::token::Token, @@ -41,13 +41,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] to: Option, /// `to_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "to_keys")] to_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `to_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "to_keys")] to_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--to-npk` and `--to-vpk`. + #[arg(long, conflicts_with_all = ["to_npk", "to_vpk"])] + to_keys: Option, /// Identifier for the recipient's private account (only used when sending to a foreign - /// private account via `--to-npk`/`--to-vpk`). + /// private account via `--to-npk`/`--to-vpk` or `--to-keys`). #[arg(long)] to_identifier: Option, /// amount - amount of balance to move. @@ -87,13 +91,17 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long)] holder: Option, /// `holder_npk` - valid 32 byte hex string. - #[arg(long)] + #[arg(long, conflicts_with = "holder_keys")] holder_npk: Option, - /// `to_vpk` - valid 33 byte hex string. - #[arg(long)] + /// `holder_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). + #[arg(long, conflicts_with = "holder_keys")] holder_vpk: Option, + /// Path to a keys file exported by `wallet account show-keys`, containing npk + /// and vpk on separate lines. Replaces `--holder-npk` and `--holder-vpk`. + #[arg(long, conflicts_with_all = ["holder_npk", "holder_vpk"])] + holder_keys: Option, /// Identifier for the holder's private account (only used when minting to a foreign - /// private account via `--holder-npk`/`--holder-vpk`). + /// private account via `--holder-npk`/`--holder-vpk` or `--holder-keys`). #[arg(long)] holder_identifier: Option, /// amount - amount of balance to mint. @@ -114,20 +122,21 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { name, total_supply, } => { + let def_mention = definition_account_id.clone(); + let sup_mention = supply_account_id.clone(); let definition_account_id = definition_account_id.resolve(wallet_core.storage())?; let supply_account_id = supply_account_id.resolve(wallet_core.storage())?; let underlying_subcommand = match (definition_account_id, supply_account_id) { - ( - AccountIdWithPrivacy::Public(definition_account_id), - AccountIdWithPrivacy::Public(supply_account_id), - ) => TokenProgramSubcommand::Create( - CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { - definition_account_id, - supply_account_id, - name, - total_supply, - }, - ), + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { + definition_account_id: def_mention, + supply_account_id: sup_mention, + name, + total_supply, + }, + ) + } ( AccountIdWithPrivacy::Public(definition_account_id), AccountIdWithPrivacy::Private(supply_account_id), @@ -170,9 +179,19 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to, to_npk, to_vpk, + to_keys, to_identifier, amount, } => { + let from_mention = from.clone(); + let to_mention = to.clone(); + let (to_npk, to_vpk) = if let Some(path) = to_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (to_npk, to_vpk) + }; + let from = from.resolve(wallet_core.storage())?; let to = to .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -192,11 +211,11 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { anyhow::bail!("List of public keys is uncomplete"); } (Some(to), None, None) => match (from, to) { - (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => { + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { TokenProgramSubcommand::Public( TokenProgramSubcommandPublic::TransferToken { - sender_account_id: from, - recipient_account_id: to, + sender_account_id: from_mention, + recipient_account_id: to_mention.expect("`wallet::cli::programs::token::Send`: Invalid to_mention account provided"), balance_to_move: amount, }, ) @@ -223,7 +242,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Private(to)) => { TokenProgramSubcommand::Shielded( TokenProgramSubcommandShielded::TransferTokenShieldedOwned { - sender_account_id: from, + sender: Some(from_mention.into_public_identity(from)), recipient_account_id: to, balance_to_move: amount, }, @@ -242,7 +261,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { ), AccountIdWithPrivacy::Public(from) => TokenProgramSubcommand::Shielded( TokenProgramSubcommandShielded::TransferTokenShieldedForeign { - sender_account_id: from, + sender: Some(from_mention.into_public_identity(from)), recipient_npk: to_npk, recipient_vpk: to_vpk, recipient_identifier: to_identifier, @@ -259,17 +278,17 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, amount, } => { + let holder_mention = holder.clone(); let definition = definition.resolve(wallet_core.storage())?; let holder = holder.resolve(wallet_core.storage())?; let underlying_subcommand = match (definition, holder) { - ( - AccountIdWithPrivacy::Public(definition), - AccountIdWithPrivacy::Public(holder), - ) => TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken { - definition_account_id: definition, - holder_account_id: holder, - amount, - }), + (AccountIdWithPrivacy::Public(definition), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken { + definition_account_id: definition, + holder_account_id: holder_mention, + amount, + }) + } ( AccountIdWithPrivacy::Private(definition), AccountIdWithPrivacy::Private(holder), @@ -309,9 +328,19 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, holder_npk, holder_vpk, + holder_keys, holder_identifier, amount, } => { + let def_mention = definition.clone(); + let holder_mention = holder.clone(); + let (holder_npk, holder_vpk) = if let Some(path) = holder_keys { + let (npk_bytes, vpk_bytes) = crate::cli::read_keys_file(&path)?; + (Some(hex::encode(npk_bytes)), Some(hex::encode(vpk_bytes))) + } else { + (holder_npk, holder_vpk) + }; + let definition = definition.resolve(wallet_core.storage())?; let holder = holder .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -331,16 +360,15 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { anyhow::bail!("List of public keys is uncomplete"); } (Some(holder), None, None) => match (definition, holder) { - ( - AccountIdWithPrivacy::Public(definition), - AccountIdWithPrivacy::Public(holder), - ) => TokenProgramSubcommand::Public( - TokenProgramSubcommandPublic::MintToken { - definition_account_id: definition, - holder_account_id: holder, - amount, - }, - ), + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::MintToken { + definition_account_id: def_mention, + holder_account_id: holder_mention.expect("`wallet::cli::programs::token::Mint`: Invalid holder_mention account provided"), + amount, + }, + ) + } ( AccountIdWithPrivacy::Private(definition), AccountIdWithPrivacy::Private(holder), @@ -430,9 +458,9 @@ pub enum TokenProgramSubcommandPublic { // Transfer tokens using the token program TransferToken { #[arg(short, long)] - sender_account_id: AccountId, + sender_account_id: CliAccountMention, #[arg(short, long)] - recipient_account_id: AccountId, + recipient_account_id: CliAccountMention, #[arg(short, long)] balance_to_move: u128, }, @@ -441,16 +469,16 @@ pub enum TokenProgramSubcommandPublic { #[arg(short, long)] definition_account_id: AccountId, #[arg(short, long)] - holder_account_id: AccountId, + holder_account_id: CliAccountMention, #[arg(short, long)] amount: u128, }, // Transfer tokens using the token program MintToken { #[arg(short, long)] - definition_account_id: AccountId, + definition_account_id: CliAccountMention, #[arg(short, long)] - holder_account_id: AccountId, + holder_account_id: CliAccountMention, #[arg(short, long)] amount: u128, }, @@ -475,7 +503,7 @@ pub enum TokenProgramSubcommandPrivate { /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -555,8 +583,8 @@ pub enum TokenProgramSubcommandDeshielded { pub enum TokenProgramSubcommandShielded { // Transfer tokens using the token program TransferTokenShieldedOwned { - #[arg(short, long)] - sender_account_id: AccountId, + #[arg(skip)] + sender: Option, #[arg(short, long)] recipient_account_id: AccountId, #[arg(short, long)] @@ -564,12 +592,12 @@ pub enum TokenProgramSubcommandShielded { }, // Transfer tokens using the token program TransferTokenShieldedForeign { - #[arg(short, long)] - sender_account_id: AccountId, + #[arg(skip)] + sender: Option, /// `recipient_npk` - valid 32 byte hex string. #[arg(long)] recipient_npk: String, - /// `recipient_vpk` - valid 33 byte hex string. + /// `recipient_vpk` - valid hex-encoded ML-KEM-768 encapsulation key (1184 bytes). #[arg(long)] recipient_vpk: String, /// Identifier for the recipient's private account. @@ -620,9 +648,9 @@ pub enum CreateNewTokenProgramSubcommand { /// Definition - public, supply - public. NewPublicDefPublicSupp { #[arg(short, long)] - definition_account_id: AccountId, + definition_account_id: CliAccountMention, #[arg(short, long)] - supply_account_id: AccountId, + supply_account_id: CliAccountMention, #[arg(short, long)] name: String, #[arg(short, long)] @@ -680,13 +708,28 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { recipient_account_id, balance_to_move, } => { - Token(wallet_core) + let sender = sender_account_id.resolve(wallet_core.storage())?; + let recipient = recipient_account_id.resolve(wallet_core.storage())?; + let ( + AccountIdWithPrivacy::Public(sender_id), + AccountIdWithPrivacy::Public(recipient_id), + ) = (sender, recipient) + else { + anyhow::bail!( + "`TokenProgramSubcommandPublic::TransferToken`: Unexpected private account received." + ); + }; + let tx_hash = Token(wallet_core) .send_transfer_transaction( - sender_account_id, - recipient_account_id, + sender_account_id.into_public_identity(sender_id), + recipient_account_id.into_public_identity(recipient_id), balance_to_move, ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } Self::BurnToken { @@ -694,9 +737,23 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { holder_account_id, amount, } => { - Token(wallet_core) - .send_burn_transaction(definition_account_id, holder_account_id, amount) + let holder = holder_account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Public(holder_id) = holder else { + anyhow::bail!( + "`TokenProgramSubcommandPublic::BurnToken`: holder account must be public." + ); + }; + let tx_hash = Token(wallet_core) + .send_burn_transaction( + definition_account_id, + holder_account_id.into_public_identity(holder_id), + amount, + ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } Self::MintToken { @@ -704,9 +761,26 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { holder_account_id, amount, } => { - Token(wallet_core) - .send_mint_transaction(definition_account_id, holder_account_id, amount) + let definition = definition_account_id.resolve(wallet_core.storage())?; + let holder = holder_account_id.resolve(wallet_core.storage())?; + let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(holder_id)) = + (definition, holder) + else { + anyhow::bail!( + "`TokenProgramSubcommandPublic::MintToken`: holder account must be public." + ); + }; + let tx_hash = Token(wallet_core) + .send_mint_transaction( + definition_account_id.into_public_identity(def_id), + holder_account_id.into_public_identity(holder_id), + amount, + ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } } @@ -764,12 +838,12 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; + let recipient_vpk = + lee_core::encryption::ViewingPublicKey::from_bytes(recipient_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_sender, _]) = Token(wallet_core) .send_transfer_transaction_private_foreign_account( @@ -876,12 +950,11 @@ impl WalletSubcommand for TokenProgramSubcommandPrivate { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; + let holder_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(holder_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, [secret_definition, _]) = Token(wallet_core) .send_mint_transaction_private_foreign_account( @@ -1021,7 +1094,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { ) -> Result { match self { Self::TransferTokenShieldedForeign { - sender_account_id, + sender, recipient_npk, recipient_vpk, recipient_identifier, @@ -1032,16 +1105,16 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_npk.copy_from_slice(&recipient_npk_res); let recipient_npk = lee_core::NullifierPublicKey(recipient_npk); - let recipient_vpk_res = hex::decode(recipient_vpk)?; - let mut recipient_vpk = [0_u8; 33]; - recipient_vpk.copy_from_slice(&recipient_vpk_res); - let recipient_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - recipient_vpk.to_vec(), - ); + let recipient_vpk_res = hex::decode(&recipient_vpk).context( + "wallet::cli::programs::token: recipient_vpk must be a valid hex string", + )?; + let recipient_vpk = + lee_core::encryption::ViewingPublicKey::from_bytes(recipient_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = Token(wallet_core) .send_transfer_transaction_shielded_foreign_account( - sender_account_id, + sender.expect("sender set during Send dispatch"), recipient_npk, recipient_vpk, recipient_identifier.unwrap_or_else(rand::random), @@ -1062,13 +1135,13 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) } Self::TransferTokenShieldedOwned { - sender_account_id, + sender, recipient_account_id, balance_to_move, } => { let (tx_hash, secret_recipient) = Token(wallet_core) .send_transfer_transaction_shielded_owned_account( - sender_account_id, + sender.expect("sender set during Send dispatch"), recipient_account_id, balance_to_move, ) @@ -1163,12 +1236,11 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { holder_npk.copy_from_slice(&holder_npk_res); let holder_npk = lee_core::NullifierPublicKey(holder_npk); - let holder_vpk_res = hex::decode(holder_vpk)?; - let mut holder_vpk = [0_u8; 33]; - holder_vpk.copy_from_slice(&holder_vpk_res); - let holder_vpk = lee_core::encryption::shared_key_derivation::Secp256k1Point( - holder_vpk.to_vec(), - ); + let holder_vpk_res = hex::decode(&holder_vpk).context( + "wallet::cli::programs::token: holder_vpk must be a valid hex string", + )?; + let holder_vpk = lee_core::encryption::ViewingPublicKey::from_bytes(holder_vpk_res) + .map_err(|e| anyhow::anyhow!("{e}"))?; let (tx_hash, _) = Token(wallet_core) .send_mint_transaction_shielded_foreign_account( @@ -1307,14 +1379,25 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { - Token(wallet_core) + let definition = definition_account_id.resolve(wallet_core.storage())?; + let supply = supply_account_id.resolve(wallet_core.storage())?; + let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(sup_id)) = + (definition, supply) + else { + anyhow::bail!("`NewPublicDefPublicSupp`: unexpected private account received."); + }; + let tx_hash = Token(wallet_core) .send_new_definition( - definition_account_id, - supply_account_id, + definition_account_id.into_public_identity(def_id), + supply_account_id.into_public_identity(sup_id), name, total_supply, ) .await?; + println!("Transaction hash is {tx_hash}"); + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + println!("Transaction data is {transfer_tx:?}"); + wallet_core.store_persistent_data()?; Ok(SubcommandReturnValue::Empty) } } diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index a31fd4db..7dece16a 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -1,5 +1,6 @@ #![expect( clippy::print_stdout, + clippy::print_stderr, reason = "This is a CLI application, printing to stdout and stderr is expected and convenient" )] #![expect( @@ -76,6 +77,8 @@ pub enum ExecutionFailureKind { AccountDataError(AccountId), #[error("Failed to build transaction: {0}")] TransactionBuildError(#[from] lee::error::LeeError), + #[error("Failed to sign transaction: {0}")] + SignError(anyhow::Error), #[error(transparent)] KeycardError(#[from] pyo3::PyErr), } @@ -269,7 +272,10 @@ impl WalletCore { } /// Set the wallet's dedicated sealing secret key. - pub const fn set_sealing_secret_key(&mut self, key: lee_core::encryption::Scalar) { + pub const fn set_sealing_secret_key( + &mut self, + key: key_protocol::key_management::secret_holders::ViewingSecretKey, + ) { self.storage.key_chain_mut().set_sealing_secret_key(key); } @@ -561,6 +567,7 @@ impl WalletCore { let acc_manager = account_manager::AccountManager::new(self, accounts).await?; let pre_states = acc_manager.pre_states(); + tx_pre_check( &pre_states .iter() @@ -574,26 +581,26 @@ impl WalletCore { instruction_data, acc_manager.account_identities(), &program.to_owned(), - ) - .unwrap(); + )?; let message = lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( acc_manager.public_account_ids(), - Vec::from_iter(acc_manager.public_account_nonces()), - private_account_keys - .iter() - .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) - .collect(), + acc_manager.public_account_nonces(), output, - ) - .unwrap(); + )?; + + let message_hash = message.hash(); + let signatures_public_keys = acc_manager + .sign_message(message_hash) + .map_err(ExecutionFailureKind::SignError)?; + + let witness_set = + lee::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts( + signatures_public_keys, + proof, + ); - let witness_set = lee::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &acc_manager.public_account_auth(), - ); let tx = PrivacyPreservingTransaction::new(message, witness_set); let shared_secrets: Vec<_> = private_account_keys @@ -648,7 +655,6 @@ impl WalletCore { let account_ids = acc_manager.public_account_ids(); let program_id = program.program.id(); let nonces = acc_manager.public_account_nonces(); - let private_keys = acc_manager.public_account_auth(); let message = lee::public_transaction::Message::new_preserialized( program_id, @@ -657,7 +663,13 @@ impl WalletCore { instruction_data, ); - let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &private_keys); + let message_hash = message.hash(); + let signatures_public_keys = acc_manager + .sign_message(message_hash) + .map_err(ExecutionFailureKind::SignError)?; + + let witness_set = + lee::public_transaction::WitnessSet::from_raw_parts(signatures_public_keys); let tx = lee::public_transaction::PublicTransaction::new(message, witness_set); @@ -723,7 +735,7 @@ impl WalletCore { .storage .key_chain() .private_account_key_chains() - .flat_map(|(_account_id, key_chain, index)| { + .flat_map(|(_account_id, key_chain, _index)| { let view_tag = EncryptedAccountData::compute_view_tag( &key_chain.nullifier_public_key, &key_chain.viewing_public_key, @@ -738,10 +750,8 @@ impl WalletCore { .filter_map(move |(ciph_id, encrypted_data)| { let ciphertext = &encrypted_data.ciphertext; let commitment = &new_commitments[ciph_id]; - let shared_secret = key_chain.calculate_shared_secret_receiver( - &encrypted_data.epk, - index.and_then(ChainIndex::index), - ); + let shared_secret = + key_chain.calculate_shared_secret_receiver(&encrypted_data.epk)?; lee_core::EncryptionScheme::decrypt( ciphertext, @@ -823,7 +833,11 @@ impl WalletCore { continue; } - let shared_secret = SharedSecretKey::new(vsk, &encrypted_data.epk); + let Some(shared_secret) = + SharedSecretKey::decapsulate(&encrypted_data.epk, &vsk.d, &vsk.z) + else { + continue; + }; let commitment = &tx.message.new_commitments[ciph_id]; if let Some((_kind, new_acc)) = lee_core::EncryptionScheme::decrypt( diff --git a/lez/wallet/src/program_facades/amm.rs b/lez/wallet/src/program_facades/amm.rs index 5c5f69b7..e1261b26 100644 --- a/lez/wallet/src/program_facades/amm.rs +++ b/lez/wallet/src/program_facades/amm.rs @@ -9,30 +9,37 @@ pub struct Amm<'wallet>(pub &'wallet WalletCore); impl Amm<'_> { pub async fn send_new_definition( &self, - user_holding_a: AccountId, - user_holding_b: AccountId, - user_holding_lp: AccountId, + user_holding_a: AccountIdentity, + user_holding_b: AccountIdentity, + user_holding_lp: AccountIdentity, balance_a: u128, balance_b: u128, ) -> Result { + let a_id = user_holding_a + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let b_id = user_holding_b + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self .0 - .get_account_public(user_holding_a) + .get_account_public(a_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let user_b_acc = self .0 - .get_account_public(user_holding_b) + .get_account_public(b_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(a_id))? .definition_id(); let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(b_id))? .definition_id(); let amm_pool = @@ -55,9 +62,9 @@ impl Amm<'_> { AccountIdentity::PublicNoSign(vault_holding_a), AccountIdentity::PublicNoSign(vault_holding_b), AccountIdentity::PublicNoSign(pool_lp), - AccountIdentity::Public(user_holding_a), - AccountIdentity::Public(user_holding_b), - AccountIdentity::Public(user_holding_lp), + user_holding_a, + user_holding_b, + user_holding_lp, ], instruction_data, &program.into(), @@ -67,30 +74,37 @@ impl Amm<'_> { pub async fn send_swap_exact_input( &self, - user_holding_a: AccountId, - user_holding_b: AccountId, + user_holding_a: AccountIdentity, + user_holding_b: AccountIdentity, swap_amount_in: u128, min_amount_out: u128, token_definition_id_in: AccountId, ) -> Result { + let a_id = user_holding_a + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let b_id = user_holding_b + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self .0 - .get_account_public(user_holding_a) + .get_account_public(a_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let user_b_acc = self .0 - .get_account_public(user_holding_b) + .get_account_public(b_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(a_id))? .definition_id(); let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(b_id))? .definition_id(); let amm_pool = @@ -114,15 +128,15 @@ impl Amm<'_> { } let user_a_signing_identity = if token_definition_id_in == definition_token_a_id { - AccountIdentity::Public(user_holding_a) + user_holding_a } else { - AccountIdentity::PublicNoSign(user_holding_a) + AccountIdentity::PublicNoSign(a_id) }; let user_b_signing_identity = if token_definition_id_in == definition_token_b_id { - AccountIdentity::Public(user_holding_b) + user_holding_b } else { - AccountIdentity::PublicNoSign(user_holding_b) + AccountIdentity::PublicNoSign(b_id) }; self.0 @@ -142,30 +156,37 @@ impl Amm<'_> { pub async fn send_swap_exact_output( &self, - user_holding_a: AccountId, - user_holding_b: AccountId, + user_holding_a: AccountIdentity, + user_holding_b: AccountIdentity, exact_amount_out: u128, max_amount_in: u128, token_definition_id_in: AccountId, ) -> Result { + let a_id = user_holding_a + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let b_id = user_holding_b + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self .0 - .get_account_public(user_holding_a) + .get_account_public(a_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let user_b_acc = self .0 - .get_account_public(user_holding_b) + .get_account_public(b_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(a_id))? .definition_id(); let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(b_id))? .definition_id(); let amm_pool = @@ -189,15 +210,15 @@ impl Amm<'_> { } let user_a_signing_identity = if token_definition_id_in == definition_token_a_id { - AccountIdentity::Public(user_holding_a) + user_holding_a } else { - AccountIdentity::PublicNoSign(user_holding_a) + AccountIdentity::PublicNoSign(a_id) }; let user_b_signing_identity = if token_definition_id_in == definition_token_b_id { - AccountIdentity::Public(user_holding_b) + user_holding_b } else { - AccountIdentity::PublicNoSign(user_holding_b) + AccountIdentity::PublicNoSign(b_id) }; self.0 @@ -217,31 +238,38 @@ impl Amm<'_> { pub async fn send_add_liquidity( &self, - user_holding_a: AccountId, - user_holding_b: AccountId, - user_holding_lp: AccountId, + user_holding_a: AccountIdentity, + user_holding_b: AccountIdentity, + user_holding_lp: AccountIdentity, min_amount_liquidity: u128, max_amount_to_add_token_a: u128, max_amount_to_add_token_b: u128, ) -> Result { + let a_id = user_holding_a + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let b_id = user_holding_b + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::amm(); let amm_program_id = Program::amm().id(); let user_a_acc = self .0 - .get_account_public(user_holding_a) + .get_account_public(a_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let user_b_acc = self .0 - .get_account_public(user_holding_b) + .get_account_public(b_id) .await .map_err(ExecutionFailureKind::SequencerError)?; let definition_token_a_id = TokenHolding::try_from(&user_a_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_a))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(a_id))? .definition_id(); let definition_token_b_id = TokenHolding::try_from(&user_b_acc.data) - .map_err(|_err| ExecutionFailureKind::AccountDataError(user_holding_b))? + .map_err(|_err| ExecutionFailureKind::AccountDataError(b_id))? .definition_id(); let amm_pool = @@ -264,9 +292,9 @@ impl Amm<'_> { AccountIdentity::PublicNoSign(vault_holding_a), AccountIdentity::PublicNoSign(vault_holding_b), AccountIdentity::PublicNoSign(pool_lp), - AccountIdentity::Public(user_holding_a), - AccountIdentity::Public(user_holding_b), - AccountIdentity::PublicNoSign(user_holding_lp), + user_holding_a, + user_holding_b, + user_holding_lp, ], instruction_data, &program.into(), @@ -278,7 +306,7 @@ impl Amm<'_> { &self, user_holding_a: AccountId, user_holding_b: AccountId, - user_holding_lp: AccountId, + user_holding_lp: AccountIdentity, remove_liquidity_amount: u128, min_amount_to_remove_token_a: u128, min_amount_to_remove_token_b: u128, @@ -325,7 +353,7 @@ impl Amm<'_> { AccountIdentity::PublicNoSign(pool_lp), AccountIdentity::PublicNoSign(user_holding_a), AccountIdentity::PublicNoSign(user_holding_b), - AccountIdentity::Public(user_holding_lp), + user_holding_lp, ], instruction_data, &program.into(), diff --git a/lez/wallet/src/program_facades/ata.rs b/lez/wallet/src/program_facades/ata.rs index 57b9c83f..e6b8fc29 100644 --- a/lez/wallet/src/program_facades/ata.rs +++ b/lez/wallet/src/program_facades/ata.rs @@ -14,9 +14,13 @@ pub struct Ata<'wallet>(pub &'wallet WalletCore); impl Ata<'_> { pub async fn send_create( &self, - owner_id: AccountId, + owner: AccountIdentity, definition_id: AccountId, ) -> Result { + let owner_id = owner + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::ata(); let ata_program_id = program.id(); let ata_id = get_associated_token_account_id( @@ -30,7 +34,7 @@ impl Ata<'_> { self.0 .send_pub_tx( vec![ - AccountIdentity::Public(owner_id), + owner, AccountIdentity::PublicNoSign(definition_id), AccountIdentity::PublicNoSign(ata_id), ], @@ -42,11 +46,15 @@ impl Ata<'_> { pub async fn send_transfer( &self, - owner_id: AccountId, + owner: AccountIdentity, definition_id: AccountId, recipient_id: AccountId, amount: u128, ) -> Result { + let owner_id = owner + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::ata(); let ata_program_id = program.id(); let sender_ata_id = get_associated_token_account_id( @@ -63,7 +71,7 @@ impl Ata<'_> { self.0 .send_pub_tx( vec![ - AccountIdentity::Public(owner_id), + owner, AccountIdentity::PublicNoSign(sender_ata_id), AccountIdentity::PublicNoSign(recipient_id), ], @@ -75,10 +83,14 @@ impl Ata<'_> { pub async fn send_burn( &self, - owner_id: AccountId, + owner: AccountIdentity, definition_id: AccountId, amount: u128, ) -> Result { + let owner_id = owner + .public_account_id() + .ok_or(ExecutionFailureKind::KeyNotFoundError)?; + let program = Program::ata(); let ata_program_id = program.id(); let holder_ata_id = get_associated_token_account_id( @@ -95,7 +107,7 @@ impl Ata<'_> { self.0 .send_pub_tx( vec![ - AccountIdentity::Public(owner_id), + owner, AccountIdentity::PublicNoSign(holder_ata_id), AccountIdentity::PublicNoSign(definition_id), ], diff --git a/lez/wallet/src/program_facades/native_token_transfer/public.rs b/lez/wallet/src/program_facades/native_token_transfer/public.rs index f66d4e57..47cbf2c0 100644 --- a/lez/wallet/src/program_facades/native_token_transfer/public.rs +++ b/lez/wallet/src/program_facades/native_token_transfer/public.rs @@ -1,131 +1,41 @@ use authenticated_transfer_core::Instruction as AuthTransferInstruction; -use common::{HashType, transaction::LeeTransaction}; -use lee::{ - AccountId, PublicTransaction, - program::Program, - public_transaction::{Message, WitnessSet}, -}; -use pyo3::exceptions::PyRuntimeError; -use sequencer_service_rpc::RpcClient as _; +use common::HashType; +use lee::program::Program; use super::NativeTokenTransfer; use crate::{ - ExecutionFailureKind, cli::CliAccountMention, helperfunctions::read_pin, signing::SigningGroups, + AccountIdentity, ExecutionFailureKind, + program_facades::native_token_transfer::auth_transfer_preparation, }; impl NativeTokenTransfer<'_> { pub async fn send_public_transfer( &self, - from: AccountId, - to: AccountId, + from: AccountIdentity, + to: AccountIdentity, balance_to_move: u128, - from_mention: &CliAccountMention, - to_mention: &CliAccountMention, ) -> Result { - let mut groups = SigningGroups::new(); - groups - .add_sender(from_mention, from, self.0) - .and_then(|()| groups.add_recipient(to_mention, to, self.0)) - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; + let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); - let program_id = Program::authenticated_transfer_program().id(); - let nonces = self - .0 - .get_accounts_nonces(groups.signing_ids()) + self.0 + .send_pub_tx_with_pre_check( + vec![from, to], + instruction_data, + &program.into(), + tx_pre_check, + ) .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = Message::try_new( - program_id, - vec![from, to], - nonces, - AuthTransferInstruction::Transfer { - amount: balance_to_move, - }, - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })? - .as_str() - .to_owned() - } else { - String::new() - }; - - let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) - })?; - - let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self - .0 - .sequencer_client - .send_transaction(LeeTransaction::Public(tx)) - .await?) } pub async fn register_account( &self, - from: AccountId, - account_mention: &CliAccountMention, + account: AccountIdentity, ) -> Result { - let nonces = self - .0 - .get_accounts_nonces(vec![from]) + let program = Program::authenticated_transfer_program(); + let instruction_data = Program::serialize_instruction(AuthTransferInstruction::Initialize)?; + + self.0 + .send_pub_tx(vec![account], instruction_data, &program.into()) .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let account_ids = vec![from]; - let program_id = Program::authenticated_transfer_program().id(); - let message = Message::try_new( - program_id, - account_ids, - nonces, - AuthTransferInstruction::Initialize, - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let mut groups = SigningGroups::new(); - groups - .add_sender(account_mention, from, self.0) - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })? - .as_str() - .to_owned() - } else { - String::new() - }; - - let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) - })?; - - let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self - .0 - .sequencer_client - .send_transaction(LeeTransaction::Public(tx)) - .await?) } } diff --git a/lez/wallet/src/program_facades/native_token_transfer/shielded.rs b/lez/wallet/src/program_facades/native_token_transfer/shielded.rs index ef074e37..002b1176 100644 --- a/lez/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/lez/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -8,16 +8,15 @@ use crate::{AccountIdentity, ExecutionFailureKind}; impl NativeTokenTransfer<'_> { pub async fn send_shielded_transfer( &self, - from: AccountId, + from: AccountIdentity, to: AccountId, balance_to_move: u128, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); - self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - AccountIdentity::Public(from), + from, self.0 .resolve_private_account(to) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, @@ -38,18 +37,17 @@ impl NativeTokenTransfer<'_> { pub async fn send_shielded_transfer_to_outer_account( &self, - from: AccountId, + from: AccountIdentity, to_npk: NullifierPublicKey, to_vpk: ViewingPublicKey, to_identifier: Identifier, balance_to_move: u128, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); - self.0 .send_privacy_preserving_tx_with_pre_check( vec![ - AccountIdentity::Public(from), + from, AccountIdentity::PrivateForeign { npk: to_npk, vpk: to_vpk, diff --git a/lez/wallet/src/program_facades/token.rs b/lez/wallet/src/program_facades/token.rs index df1b14c6..2170c046 100644 --- a/lez/wallet/src/program_facades/token.rs +++ b/lez/wallet/src/program_facades/token.rs @@ -10,8 +10,8 @@ pub struct Token<'wallet>(pub &'wallet WalletCore); impl Token<'_> { pub async fn send_new_definition( &self, - definition_account_id: AccountId, - supply_account_id: AccountId, + definition: AccountIdentity, + supply: AccountIdentity, name: String, total_supply: u128, ) -> Result { @@ -21,14 +21,7 @@ impl Token<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_pub_tx( - vec![ - AccountIdentity::Public(definition_account_id), - AccountIdentity::Public(supply_account_id), - ], - instruction_data, - &program.into(), - ) + .send_pub_tx(vec![definition, supply], instruction_data, &program.into()) .await } @@ -131,8 +124,8 @@ impl Token<'_> { pub async fn send_transfer_transaction( &self, - sender_account_id: AccountId, - recipient_account_id: AccountId, + sender: AccountIdentity, + recipient: AccountIdentity, amount: u128, ) -> Result { let program = Program::token(); @@ -143,14 +136,7 @@ impl Token<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_pub_tx( - vec![ - AccountIdentity::Public(sender_account_id), - AccountIdentity::Public(recipient_account_id), - ], - instruction_data, - &program.into(), - ) + .send_pub_tx(vec![sender, recipient], instruction_data, &program.into()) .await } @@ -261,7 +247,7 @@ impl Token<'_> { pub async fn send_transfer_transaction_shielded_owned_account( &self, - sender_account_id: AccountId, + sender: AccountIdentity, recipient_account_id: AccountId, amount: u128, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { @@ -270,11 +256,10 @@ impl Token<'_> { }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); - self.0 .send_privacy_preserving_tx( vec![ - AccountIdentity::Public(sender_account_id), + sender, self.0 .resolve_private_account(recipient_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?, @@ -294,7 +279,7 @@ impl Token<'_> { pub async fn send_transfer_transaction_shielded_foreign_account( &self, - sender_account_id: AccountId, + sender: AccountIdentity, recipient_npk: NullifierPublicKey, recipient_vpk: ViewingPublicKey, recipient_identifier: Identifier, @@ -305,11 +290,10 @@ impl Token<'_> { }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); - self.0 .send_privacy_preserving_tx( vec![ - AccountIdentity::Public(sender_account_id), + sender, AccountIdentity::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, @@ -332,7 +316,7 @@ impl Token<'_> { pub async fn send_burn_transaction( &self, definition_account_id: AccountId, - holder_account_id: AccountId, + holder: AccountIdentity, amount: u128, ) -> Result { let program = Program::token(); @@ -344,10 +328,7 @@ impl Token<'_> { self.0 .send_pub_tx( - vec![ - AccountIdentity::PublicNoSign(definition_account_id), - AccountIdentity::Public(holder_account_id), - ], + vec![AccountIdentity::PublicNoSign(definition_account_id), holder], instruction_data, &program.into(), ) @@ -456,8 +437,8 @@ impl Token<'_> { pub async fn send_mint_transaction( &self, - definition_account_id: AccountId, - holder_account_id: AccountId, + definition: AccountIdentity, + holder: AccountIdentity, amount: u128, ) -> Result { let program = Program::token(); @@ -468,14 +449,7 @@ impl Token<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); self.0 - .send_pub_tx( - vec![ - AccountIdentity::Public(definition_account_id), - AccountIdentity::Public(holder_account_id), - ], - instruction_data, - &program.into(), - ) + .send_pub_tx(vec![definition, holder], instruction_data, &program.into()) .await } diff --git a/lez/wallet/src/signing.rs b/lez/wallet/src/signing.rs index 1431a85a..505dedd9 100644 --- a/lez/wallet/src/signing.rs +++ b/lez/wallet/src/signing.rs @@ -1,114 +1,33 @@ -use anyhow::Result; use keycard_wallet::{KeycardWallet, python_path}; -use lee::{AccountId, PrivateKey, PublicKey, Signature}; +use pyo3::Python; -use crate::{WalletCore, cli::CliAccountMention}; - -/// Groups transaction signers by type to minimise Python GIL acquisition. -/// -/// Local signers are signed in pure Rust; all keycard signers share a single Python session -/// with one `connect` / `close_session` pair. -#[derive(Default)] -pub struct SigningGroups { - local: Vec<(AccountId, PrivateKey)>, - keycard: Vec<(AccountId, String)>, +/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction. +pub struct KeycardSessionContext { + pin: String, + wallet: Option, } -impl SigningGroups { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Add a sender. Keycard paths are queued for the hardware session; local accounts - /// have their signing key resolved eagerly. Errors if no key is found. - pub fn add_sender( - &mut self, - mention: &CliAccountMention, - account_id: AccountId, - wallet_core: &WalletCore, - ) -> Result<()> { - if let CliAccountMention::KeyPath(path) = mention { - self.keycard.push((account_id, path.clone())); - return Ok(()); +impl KeycardSessionContext { + pub fn new(pin: impl Into) -> Self { + Self { + pin: pin.into(), + wallet: None, } - let key = wallet_core - .storage() - .key_chain() - .pub_account_signing_key(account_id) - .ok_or_else(|| anyhow::anyhow!("signing key not found for account {account_id}"))? - .clone(); - self.local.push((account_id, key)); - Ok(()) } - /// Add a recipient. Same as [`add_sender`] but silently skips accounts with no local - /// key and no keycard path — they are foreign and require neither a signature nor a nonce. - pub fn add_recipient( - &mut self, - mention: &CliAccountMention, - account_id: AccountId, - wallet_core: &WalletCore, - ) -> Result<()> { - if let CliAccountMention::KeyPath(path) = mention { - self.keycard.push((account_id, path.clone())); - return Ok(()); + pub fn get_or_connect(&mut self, py: Python<'_>) -> pyo3::PyResult<&KeycardWallet> { + if self.wallet.is_none() { + python_path::add_python_path(py)?; + let wallet = KeycardWallet::new(py)?; + wallet.connect(py, &self.pin)?; + self.wallet = Some(wallet); } - if let Some(key) = wallet_core - .storage() - .key_chain() - .pub_account_signing_key(account_id) - { - self.local.push((account_id, key.clone())); + Ok(self.wallet.as_ref().expect("wallet was just inserted")) + } + + pub fn close(self, py: Python<'_>) { + if let Some(w) = self.wallet { + let _res = w.close_session(py); } - Ok(()) - } - - /// Returns `true` when a PIN is required (at least one keycard signer is present). - #[must_use] - pub const fn needs_pin(&self) -> bool { - !self.keycard.is_empty() - } - - /// Account IDs that require a nonce (every non-foreign signer). - #[must_use] - pub fn signing_ids(&self) -> Vec { - self.local - .iter() - .map(|(id, _)| *id) - .chain(self.keycard.iter().map(|(id, _)| *id)) - .collect() - } - - /// Sign `hash` for every account in the group. - /// - /// Local accounts are signed in pure Rust. Keycard accounts share one Python session. - pub fn sign_all(&self, hash: &[u8; 32], pin: &str) -> Result> { - let mut sigs: Vec<(Signature, PublicKey)> = self - .local - .iter() - .map(|(_, key)| { - ( - Signature::new(key, hash), - PublicKey::new_from_private_key(key), - ) - }) - .collect(); - - if !self.keycard.is_empty() { - pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> { - python_path::add_python_path(py)?; - let wallet = KeycardWallet::new(py)?; - wallet.connect(py, pin)?; - for (_, path) in &self.keycard { - sigs.push(wallet.sign_message_for_path(py, path, hash)?); - } - drop(wallet.close_session(py)); - Ok(()) - }) - .map_err(anyhow::Error::from)?; - } - - Ok(sigs) } } diff --git a/lez/wallet/src/storage/key_chain.rs b/lez/wallet/src/storage/key_chain.rs index c85f5551..f73833eb 100644 --- a/lez/wallet/src/storage/key_chain.rs +++ b/lez/wallet/src/storage/key_chain.rs @@ -6,7 +6,7 @@ use key_protocol::key_management::{ KeyChain, group_key_holder::GroupKeyHolder, key_tree::{KeyTreePrivate, KeyTreePublic, chain_index::ChainIndex, traits::KeyTreeNode as _}, - secret_holders::SeedHolder, + secret_holders::{SeedHolder, ViewingSecretKey}, }; use lee::{Account, AccountId}; use lee_core::{Identifier, PrivateAccountKind}; @@ -79,7 +79,7 @@ pub struct UserKeyChain { /// 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. - sealing_secret_key: Option, + sealing_secret_key: Option, } impl UserKeyChain { @@ -509,12 +509,12 @@ impl UserKeyChain { /// Returns the sealing secret key for GMS distribution, if it exists. #[must_use] - pub const fn sealing_secret_key(&self) -> Option { - self.sealing_secret_key + pub const fn sealing_secret_key(&self) -> Option<&ViewingSecretKey> { + self.sealing_secret_key.as_ref() } /// Sets the sealing secret key for GMS distribution. - pub const fn set_sealing_secret_key(&mut self, key: lee_core::encryption::Scalar) { + pub const fn set_sealing_secret_key(&mut self, key: ViewingSecretKey) { self.sealing_secret_key = Some(key); } @@ -584,7 +584,7 @@ impl UserKeyChain { KeyChainPersistentData { accounts, - sealing_secret_key: *sealing_secret_key, + sealing_secret_key: sealing_secret_key.clone(), group_key_holders: group_key_holders.clone(), shared_private_accounts: shared_private_accounts.clone(), } diff --git a/lez/wallet/src/storage/persistent.rs b/lez/wallet/src/storage/persistent.rs index 766e9a90..01d6b166 100644 --- a/lez/wallet/src/storage/persistent.rs +++ b/lez/wallet/src/storage/persistent.rs @@ -5,6 +5,7 @@ use key_protocol::key_management::{ key_tree::{ chain_index::ChainIndex, keys_private::ChildKeysPrivate, keys_public::ChildKeysPublic, }, + secret_holders::ViewingSecretKey, }; use serde::{Deserialize, Serialize}; use testnet_initial_state::{PrivateAccountPrivateInitialData, PublicAccountPrivateInitialData}; @@ -26,7 +27,7 @@ pub struct PersistentStorage { pub struct KeyChainPersistentData { pub accounts: Vec, #[serde(default)] - pub sealing_secret_key: Option, + pub sealing_secret_key: Option, #[serde(default)] pub group_key_holders: BTreeMap, #[serde(default)] diff --git a/program_methods/guest/src/bin/bridge.rs b/program_methods/guest/src/bin/bridge.rs index 4d983439..eb082c7c 100644 --- a/program_methods/guest/src/bin/bridge.rs +++ b/program_methods/guest/src/bin/bridge.rs @@ -33,6 +33,7 @@ fn main() { let chained_calls = match instruction { Instruction::Deposit { + l1_deposit_op_id: _, vault_program_id, recipient_id, amount, diff --git a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs index 621a461c..8c8ec2a4 100644 --- a/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs +++ b/program_methods/guest/src/bin/privacy_preserving_circuit/output.rs @@ -1,7 +1,7 @@ use lee_core::{ - Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, InputAccountIdentity, - MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, - PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, + Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, + EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey, + NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, account::{Account, AccountId, Nonce}, compute_digest_for_path, }; @@ -17,7 +17,7 @@ pub fn compute_circuit_output( let mut output = PrivacyPreservingCircuitOutput { public_pre_states: Vec::new(), public_post_states: Vec::new(), - ciphertexts: Vec::new(), + encrypted_private_post_states: Vec::new(), new_commitments: Vec::new(), new_nullifiers: Vec::new(), block_validity_window, @@ -40,6 +40,8 @@ pub fn compute_circuit_output( output.public_post_states.push(post_state); } InputAccountIdentity::PrivateAuthorizedInit { + epk, + view_tag, ssk, nsk, identifier, @@ -71,11 +73,15 @@ pub fn compute_circuit_output( &account_id, &PrivateAccountKind::Regular(*identifier), ssk, + epk, + *view_tag, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivateAuthorizedUpdate { + epk, + view_tag, ssk, nsk, membership_proof, @@ -105,11 +111,15 @@ pub fn compute_circuit_output( &account_id, &PrivateAccountKind::Regular(*identifier), ssk, + epk, + *view_tag, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivateUnauthorized { + epk, + view_tag, npk, ssk, identifier, @@ -140,11 +150,15 @@ pub fn compute_circuit_output( &account_id, &PrivateAccountKind::Regular(*identifier), ssk, + epk, + *view_tag, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivatePdaInit { + epk, + view_tag, npk: _, ssk, identifier, @@ -187,11 +201,15 @@ pub fn compute_circuit_output( identifier: *identifier, }, ssk, + epk, + *view_tag, new_nullifier, new_nonce, ); } InputAccountIdentity::PrivatePdaUpdate { + epk, + view_tag, ssk, nsk, membership_proof, @@ -231,6 +249,8 @@ pub fn compute_circuit_output( identifier: *identifier, }, ssk, + epk, + *view_tag, new_nullifier, new_nonce, ); @@ -243,7 +263,7 @@ pub fn compute_circuit_output( #[expect( clippy::too_many_arguments, - reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial" + reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial" )] fn emit_private_output( output: &mut PrivacyPreservingCircuitOutput, @@ -252,6 +272,8 @@ fn emit_private_output( account_id: &AccountId, kind: &PrivateAccountKind, shared_secret: &SharedSecretKey, + epk: &EphemeralPublicKey, + view_tag: u8, new_nullifier: (Nullifier, CommitmentSetDigest), new_nonce: Nonce, ) { @@ -270,7 +292,13 @@ fn emit_private_output( ); output.new_commitments.push(commitment_post); - output.ciphertexts.push(encrypted_account); + output + .encrypted_private_post_states + .push(EncryptedAccountData { + ciphertext: encrypted_account, + epk: epk.clone(), + view_tag, + }); *output_index = output_index .checked_add(1) .unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); diff --git a/programs/bridge/core/src/lib.rs b/programs/bridge/core/src/lib.rs index 4a4e9fd8..1e1bff4f 100644 --- a/programs/bridge/core/src/lib.rs +++ b/programs/bridge/core/src/lib.rs @@ -12,6 +12,9 @@ pub enum Instruction { /// - Bridge PDA account /// - Recipient vault PDA account Deposit { + /// Deposit OP ID from L1, stored here to pin each [`Deposit`](Instruction::Deposit) to a + /// Deposit Event on L1. + l1_deposit_op_id: [u8; 32], vault_program_id: ProgramId, recipient_id: AccountId, amount: u128, diff --git a/tools/crypto_primitives_bench/benches/primitives.rs b/tools/crypto_primitives_bench/benches/primitives.rs index c7584caa..11c11d9b 100644 --- a/tools/crypto_primitives_bench/benches/primitives.rs +++ b/tools/crypto_primitives_bench/benches/primitives.rs @@ -3,7 +3,7 @@ //! Measures: //! - `KeyChain::new_os_random` (mnemonic → SSK → NSK/VSK + public keys) //! - `KeyChain::new_mnemonic` (same, but mnemonic exposed) -//! - `SharedSecretKey::new` (Diffie-Hellman shared key derivation, the per-recipient cost) +//! - `SharedSecretKey::encapsulate` (ML-KEM-768 encapsulation, the per-recipient cost) //! - `EncryptionScheme::encrypt` / `decrypt` (Account note encryption) use std::time::Duration; @@ -13,10 +13,8 @@ use key_protocol::key_management::KeyChain; use lee_core::{ Commitment, EncryptionScheme, SharedSecretKey, account::{Account, AccountId}, - encryption::{EphemeralPublicKey, EphemeralSecretKey}, program::PrivateAccountKind, }; -use rand::{RngCore as _, rngs::OsRng}; fn bench_keychain(c: &mut Criterion) { let mut g = c.benchmark_group("keychain"); @@ -37,34 +35,22 @@ fn bench_shared_secret_key(c: &mut Criterion) { let mut g = c.benchmark_group("shared_secret_key"); g.sample_size(50).noise_threshold(0.05); - g.bench_function("sender_dh", |b| { - b.iter(|| { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - let _epk = EphemeralPublicKey::from(&esk); - SharedSecretKey::new(esk, &vpk) - }); + g.bench_function("sender_encapsulate", |b| { + b.iter(|| SharedSecretKey::encapsulate(&vpk)); }); g.finish(); } fn bench_encryption(c: &mut Criterion) { // One-time setup: a fixed Account/Commitment and a SharedSecretKey to bench - // encrypt/decrypt over a representative note. ESK gen is excluded from the - // measured loop (covered by the SharedSecretKey bench above). + // encrypt/decrypt over a representative note. Encapsulation cost is covered + // by the SharedSecretKey bench above. let recipient_kc = KeyChain::new_os_random(); - let vpk = recipient_kc.viewing_public_key; let npk = recipient_kc.nullifier_public_key; let account = Account::default(); let account_id = AccountId::for_regular_private_account(&npk, 0); let commitment = Commitment::new(&account_id, &account); - let shared = { - let mut bytes = [0_u8; 32]; - OsRng.fill_bytes(&mut bytes); - let esk: EphemeralSecretKey = bytes; - SharedSecretKey::new(esk, &vpk) - }; + let (shared, _epk) = SharedSecretKey::encapsulate(&recipient_kc.viewing_public_key); let kind = PrivateAccountKind::Regular(0_u128); let output_index: u32 = 0; @@ -73,7 +59,6 @@ fn bench_encryption(c: &mut Criterion) { g.bench_function("encrypt", |b| { b.iter(|| EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index)); }); - // One ciphertext for the decrypt bench (encrypt is deterministic given inputs). let ct = EncryptionScheme::encrypt(&account, &kind, &shared, &commitment, output_index); g.bench_function("decrypt", |b| { b.iter(|| EncryptionScheme::decrypt(&ct, &shared, &commitment, output_index)); diff --git a/tools/cycle_bench/src/main.rs b/tools/cycle_bench/src/main.rs index 914d68c5..bed61f92 100644 --- a/tools/cycle_bench/src/main.rs +++ b/tools/cycle_bench/src/main.rs @@ -9,11 +9,14 @@ #![expect( clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_precision_loss, clippy::float_arithmetic, clippy::missing_const_for_fn, clippy::non_ascii_literal, clippy::print_stderr, clippy::print_stdout, + clippy::suboptimal_flops, reason = "Bench tool: matches test-style fixture code" )] @@ -68,6 +71,13 @@ struct BenchResult { user_cycles: u64, segments: usize, exec_stats: Stats, + /// Compute-only execution time (ms): best-of-N executor wall-time minus the calibrated + /// host-side fixed per-call overhead. Filled after the calibration fit over all cases. + net_compute_ms: Option, + /// Deterministic model prediction of compute time (ms): `user_cycles * slope` from the + /// calibration fit. Pure function of the deterministic cycle count and the pinned-hardware + /// throughput, so it reproduces across re-runs where raw wall-time does not. + calibrated_ms: Option, /// Stats over prover.prove(env, elf) wall-clock samples. Only populated when --prove is set. /// Single-sample (n=1) when --prove is on without explicit repetition, since proving is slow. prove_stats: Option, @@ -81,6 +91,89 @@ struct BenchResult { prove_segments: Option, } +/// Linear calibration of executor wall-time against deterministic user cycles, +/// fitted across all standalone cases as `best_ms = intercept_ms + slope_ms_per_cycle * +/// user_cycles`. +/// +/// The intercept is the host-side fixed per-call cost (ELF parse, `ExecutorEnv` build) that is +/// outside the cycle count and does not scale with the instruction's work. The slope is the +/// per-cycle execution rate on the pinned box; its reciprocal is the throughput the tokenomics +/// fee model denominates public execution in, and is the public-side counterpart to the flat +/// `G_verify` verify cost. The intercept is an ELF-size-averaged constant, so `net_compute_ms` +/// is a first-order decomposition, not a mechanistic per-program overhead. +#[derive(Debug, Serialize, Clone, Copy)] +struct Calibration { + /// Cases the fit was computed over. + n: usize, + /// Slope: milliseconds of executor wall-time per user cycle. + slope_ms_per_cycle: f64, + /// Intercept: host-side fixed per-call overhead in milliseconds. + intercept_ms: f64, + /// Reciprocal of the slope: cycles executed per millisecond on the pinned box. + throughput_cycles_per_ms: f64, + /// Coefficient of determination of the fit (1.0 = perfect linear fit). + r2: f64, +} + +impl Calibration { + /// Ordinary least squares of `best_ms` (y) on `user_cycles` (x) across `results`. + /// The fit uses best-of-N rather than the mean so a single OS scheduling spike in one + /// case cannot tilt the slope; best-of-N is the per-case noise floor and reproduces + /// run-to-run, which is what a pinned-hardware throughput constant needs. + /// Returns `None` when there are fewer than two distinct cycle counts to fit a line. + fn fit(results: &[BenchResult]) -> Option { + let n = results.len(); + if n < 2 { + return None; + } + let xs: Vec = results.iter().map(|r| r.user_cycles as f64).collect(); + let ys: Vec = results.iter().map(|r| r.exec_stats.best_ms).collect(); + let nf = n as f64; + let sum_x: f64 = xs.iter().sum(); + let sum_y: f64 = ys.iter().sum(); + let sum_xy: f64 = xs.iter().zip(&ys).map(|(x, y)| x * y).sum(); + let sum_xx: f64 = xs.iter().map(|x| x * x).sum(); + let denom = nf * sum_xx - sum_x.powi(2); + if denom.abs() < f64::EPSILON { + return None; + } + let slope = (nf * sum_xy - sum_x * sum_y) / denom; + let intercept = (sum_y - slope * sum_x) / nf; + let mean_y = sum_y / nf; + let ss_tot: f64 = ys.iter().map(|y| (y - mean_y).powi(2)).sum(); + let ss_res: f64 = xs + .iter() + .zip(&ys) + .map(|(x, y)| (y - (intercept + slope * x)).powi(2)) + .sum(); + // ss_tot ≈ 0 means every best_ms is identical; the ratio is 0/0. We report 1.0 (a flat + // line fits a flat cloud exactly). This is a degenerate guard, not a real-data path: the + // bench cases span a wide cycle range, so ss_tot is large in practice. + let r2 = if ss_tot.abs() < f64::EPSILON { + 1.0 + } else { + 1.0 - ss_res / ss_tot + }; + let throughput_cycles_per_ms = if slope.abs() < f64::EPSILON { + 0.0 + } else { + 1.0 / slope + }; + Some(Self { + n, + slope_ms_per_cycle: slope, + intercept_ms: intercept, + throughput_cycles_per_ms, + r2, + }) + } + + /// Compute-time prediction for a cycle count: `slope * user_cycles` (overhead excluded). + fn calibrated_ms(&self, user_cycles: u64) -> f64 { + self.slope_ms_per_cycle * user_cycles as f64 + } +} + struct Case { program: &'static str, instruction_label: &'static str, @@ -185,6 +278,8 @@ impl Case { user_cycles: info.cycles(), segments: info.segments.len(), exec_stats, + net_compute_ms: None, + calibrated_ms: None, prove_stats, prove_total_cycles, prove_user_cycles, @@ -495,12 +590,23 @@ fn main() -> Result<()> { )?, ]; - let results: Vec = cases + let mut results: Vec = cases .into_iter() .map(|c| c.run(prove, exec_iters)) .collect::>>()?; + let calibration = Calibration::fit(&results); + if let Some(cal) = calibration { + for r in &mut results { + r.calibrated_ms = Some(cal.calibrated_ms(r.user_cycles)); + r.net_compute_ms = Some(r.exec_stats.best_ms - cal.intercept_ms); + } + } + print_table(&results, prove); + if let Some(cal) = calibration { + print_calibration(&cal); + } #[cfg(feature = "ppe")] let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() }; @@ -525,6 +631,7 @@ fn main() -> Result<()> { } let combined = serde_json::json!({ "standalone": results, + "calibration": calibration, "ppe": ppe_results, }); std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?; @@ -533,6 +640,24 @@ fn main() -> Result<()> { Ok(()) } +fn print_calibration(cal: &Calibration) { + println!("\npublic-execution ms calibration (pinned hardware):"); + println!( + " fit: best_ms = {:.4} + {:.3e} * user_cycles (n={}, R²={:.4})", + cal.intercept_ms, cal.slope_ms_per_cycle, cal.n, cal.r2, + ); + println!( + " throughput: {:.0} cycles/ms", + cal.throughput_cycles_per_ms, + ); + println!( + " fixed overhead: {:.3} ms host-side per call (ELF parse + env build, off-cycle)", + cal.intercept_ms, + ); + println!(" calib_ms = user_cycles / throughput (compute only, overhead excluded)"); + println!(" net_ms = best exec_ms - fixed overhead (measured compute, overhead stripped)"); +} + fn print_table(results: &[BenchResult], prove: bool) { let pw = results .iter() @@ -555,15 +680,28 @@ fn print_table(results: &[BenchResult], prove: bool) { .unwrap_or(0) .max("exec_ms (best / mean ± stdev)".len()); + let dw = 10_usize; println!( - "{:cw$} {:>sw$} {:cw$} {:>sw$} {:dw$} {:>dw$}", + "program", + "instruction", + "user_cycles", + "segments", + "exec_ms (best / mean ± stdev)", + "calib_ms", + "net_ms", ); - println!("{}", "-".repeat(pw + iw + cw + sw + exec_w + 8)); + println!("{}", "-".repeat(pw + iw + cw + sw + exec_w + 2 * dw + 12)); for r in results { + let calib = r + .calibrated_ms + .map_or_else(|| "-".to_owned(), |v| format!("{v:.2}")); + let net = r + .net_compute_ms + .map_or_else(|| "-".to_owned(), |v| format!("{v:.2}")); println!( - "{:cw$} {:>sw$} {:cw$} {:>sw$} {:dw$} {:>dw$}", + r.program, r.instruction, r.user_cycles, r.segments, r.exec_stats, calib, net, ); } @@ -595,3 +733,78 @@ fn print_table(results: &[BenchResult], prove: bool) { } } } + +#[cfg(test)] +mod tests { + use cycle_bench::stats::Stats; + + use super::{BenchResult, Calibration}; + + /// Minimal `BenchResult` carrying only the fields the calibration fit reads: + /// `user_cycles` (x) and `exec_stats.best_ms` (y). + fn point(user_cycles: u64, best_ms: f64) -> BenchResult { + BenchResult { + program: "test", + instruction: "test", + user_cycles, + segments: 1, + exec_stats: Stats::from_samples(&[best_ms]), + net_compute_ms: None, + calibrated_ms: None, + prove_stats: None, + prove_total_cycles: None, + prove_user_cycles: None, + prove_paging_cycles: None, + prove_segments: None, + } + } + + fn close(a: f64, b: f64) -> bool { + (a - b).abs() < 1e-9 + } + + #[test] + fn fit_recovers_a_known_line() { + // best_ms = 10 + 0.001 * user_cycles -> slope 1e-3, intercept 10, throughput 1000. + let results = [point(1000, 11.0), point(2000, 12.0), point(3000, 13.0)]; + let cal = Calibration::fit(&results).expect("fit over three points"); + + assert!( + close(cal.slope_ms_per_cycle, 0.001), + "slope {}", + cal.slope_ms_per_cycle + ); + assert!( + close(cal.intercept_ms, 10.0), + "intercept {}", + cal.intercept_ms + ); + assert!( + close(cal.throughput_cycles_per_ms, 1000.0), + "throughput {}", + cal.throughput_cycles_per_ms, + ); + assert!(close(cal.r2, 1.0), "r2 {}", cal.r2); + assert_eq!(cal.n, 3); + // calibrated_ms is the overhead-excluded compute prediction: slope * cycles. + assert!( + close(cal.calibrated_ms(2000), 2.0), + "calib {}", + cal.calibrated_ms(2000) + ); + } + + #[test] + fn fit_needs_at_least_two_points() { + assert!(Calibration::fit(&[]).is_none()); + assert!(Calibration::fit(&[point(1000, 11.0)]).is_none()); + } + + #[test] + fn fit_with_identical_cycle_counts_returns_none() { + // Zero spread in x leaves the slope undetermined; the fit must decline rather than divide + // by zero. + let results = [point(1000, 11.0), point(1000, 12.0)]; + assert!(Calibration::fit(&results).is_none()); + } +} diff --git a/tools/integration_bench/src/scenarios/amm.rs b/tools/integration_bench/src/scenarios/amm.rs index a6001ebe..483010eb 100644 --- a/tools/integration_bench/src/scenarios/amm.rs +++ b/tools/integration_bench/src/scenarios/amm.rs @@ -180,6 +180,7 @@ async fn timed_token_send( to: Some(public_mention(to_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount, }), diff --git a/tools/integration_bench/src/scenarios/fanout.rs b/tools/integration_bench/src/scenarios/fanout.rs index 332e83f6..d230523b 100644 --- a/tools/integration_bench/src/scenarios/fanout.rs +++ b/tools/integration_bench/src/scenarios/fanout.rs @@ -50,6 +50,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/parallel.rs b/tools/integration_bench/src/scenarios/parallel.rs index 24265bc3..3e69ad2b 100644 --- a/tools/integration_bench/src/scenarios/parallel.rs +++ b/tools/integration_bench/src/scenarios/parallel.rs @@ -69,6 +69,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(sender_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER * 5, }), @@ -97,6 +98,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(*recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: AMOUNT_PER_TRANSFER, }), diff --git a/tools/integration_bench/src/scenarios/private.rs b/tools/integration_bench/src/scenarios/private.rs index e46d6059..be6bb33b 100644 --- a/tools/integration_bench/src/scenarios/private.rs +++ b/tools/integration_bench/src/scenarios/private.rs @@ -46,6 +46,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_a)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -64,6 +65,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(public_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 100, }), @@ -82,6 +84,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_b)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 200, }), diff --git a/tools/integration_bench/src/scenarios/token.rs b/tools/integration_bench/src/scenarios/token.rs index bfc41d5d..3cff19e9 100644 --- a/tools/integration_bench/src/scenarios/token.rs +++ b/tools/integration_bench/src/scenarios/token.rs @@ -41,6 +41,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(public_mention(recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 1_000, }), @@ -61,6 +62,7 @@ pub async fn run(ctx: &mut TestContext) -> Result { to: Some(private_mention(private_recipient_id)), to_npk: None, to_vpk: None, + to_keys: None, to_identifier: Some(0), amount: 500, }),