diff --git a/Cargo.lock b/Cargo.lock index 352a5011..65e510de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2038,7 +2038,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]] @@ -2542,7 +2542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3917,15 +3917,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" @@ -6198,15 +6189,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" @@ -7524,37 +7506,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", @@ -7562,9 +7539,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", @@ -7574,13 +7551,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", ] @@ -8583,7 +8559,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9630,7 +9606,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10520,12 +10496,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" @@ -11056,7 +11026,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index def9d5f0..b8e12e8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,7 +165,7 @@ actix-web = { version = "4.13.0", default-features = false, features = [ ] } 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"] } diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index 046f21bb..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 cae6ed4e..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 cebe1042..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 9810c6ac..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 1124913f..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 ca45b686..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 118f19d3..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 f3ecb0e9..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 66f6d5b6..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 a36fbbc8..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 7628b459..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 3884195d..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 6afda3a5..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 dd2db03f..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 58291896..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 bb2feabb..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 b0f3a67c..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 5cda6717..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 898cea24..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 a74d931d..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 8effbf85..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 e4aa09dc..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 f0d031b8..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 589bc620..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 8c0d1350..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 8bcccb4e..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 70392d9c..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 62b8af74..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 c4b115ab..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 ae599f60..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 1435dea9..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 bc479a80..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 e0d52639..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 f34373ff..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_spend_proxy.bin b/artifacts/test_program_methods/pda_spend_proxy.bin index 33e38f00..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 65879893..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 7652eaa7..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 81d9a7e1..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 ef9b3006..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 ba50eebd..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 32a8b581..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 85a38041..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 99f379c5..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/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/auth_transfer/private.rs b/integration_tests/tests/auth_transfer/private.rs index 9eea9b04..45a1b085 100644 --- a/integration_tests/tests/auth_transfer/private.rs +++ b/integration_tests/tests/auth_transfer/private.rs @@ -11,7 +11,7 @@ use lee::{ privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, }; use lee_core::{ - InputAccountIdentity, NullifierPublicKey, + EncryptedAccountData, InputAccountIdentity, NullifierPublicKey, account::AccountWithMetadata, encryption::{EphemeralPublicKey, ViewingPublicKey}, }; @@ -665,9 +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 = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); + let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap(); let ssk = SharedSecretKey([55_u8; 32]); - let _epk = EphemeralPublicKey(vec![55_u8; 1088]); + 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) @@ -712,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/bridge.rs b/integration_tests/tests/bridge.rs index 81f62f2b..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, @@ -150,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")?; @@ -450,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/private_pda.rs b/integration_tests/tests/private_pda.rs index ea7cafab..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, @@ -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); diff --git a/integration_tests/tests/tps.rs b/integration_tests/tests/tps.rs index daf52609..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, }; @@ -301,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, @@ -315,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/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/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/encryption/mod.rs b/lee/state_machine/core/src/encryption/mod.rs index 37745d4f..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, MlKem768EncapsulationKey, 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( 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 a3b2fd32..5c982c6f 100644 --- a/lee/state_machine/core/src/encryption/shared_key_derivation.rs +++ b/lee/state_machine/core/src/encryption/shared_key_derivation.rs @@ -2,12 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; use serde::{Deserialize, Serialize}; -use crate::SharedSecretKey; - -/// 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); +use crate::{EphemeralPublicKey, SharedSecretKey}; /// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate). #[derive( 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 cebef4cf..87c7c5bb 100644 --- a/lee/state_machine/src/privacy_preserving_transaction/circuit.rs +++ b/lee/state_machine/src/privacy_preserving_transaction/circuit.rs @@ -178,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}, }; @@ -201,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"), @@ -210,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(); @@ -257,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, @@ -274,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, @@ -356,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 @@ -364,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, @@ -378,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, @@ -390,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, @@ -432,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, @@ -461,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, @@ -508,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, @@ -561,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, @@ -618,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, @@ -647,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, @@ -691,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, @@ -735,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(), @@ -789,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(), @@ -827,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, @@ -870,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 6a289a0a..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, 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 4b74cf55..c7152917 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -418,8 +418,8 @@ 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, ViewingPublicKey}, program::{ @@ -613,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(); @@ -1376,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, @@ -1388,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(); @@ -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, &[]); @@ -1494,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 @@ -1507,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, &[]); @@ -1630,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(); @@ -1992,6 +2112,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2003,6 +2128,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2048,6 +2178,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2059,6 +2194,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2104,6 +2244,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2115,6 +2260,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2160,6 +2310,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2171,6 +2326,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2216,6 +2376,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2227,6 +2392,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2270,6 +2440,11 @@ pub mod tests { Program::serialize_instruction(10_u128).unwrap(), vec![ InputAccountIdentity::PrivateAuthorizedUpdate { + 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], @@ -2281,6 +2456,11 @@ pub 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: SharedSecretKey::encapsulate_deterministic( &recipient_keys.vpk(), @@ -2326,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, @@ -2359,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, @@ -2370,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()); } @@ -2400,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, @@ -2437,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, @@ -2477,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, @@ -2516,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, @@ -2564,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, @@ -2652,12 +2848,22 @@ pub mod tests { 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![]), @@ -3003,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 @@ -3016,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); @@ -3129,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 @@ -3137,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 @@ -3149,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); @@ -3406,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, @@ -3415,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, &[]); @@ -3454,6 +3661,11 @@ pub mod tests { 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, @@ -3462,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); @@ -3506,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, @@ -3514,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); @@ -3553,6 +3758,11 @@ pub mod tests { 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, @@ -3630,6 +3840,11 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { + 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, @@ -3657,6 +3872,11 @@ pub mod tests { vec![private_account], Program::serialize_instruction(instruction).unwrap(), vec![InputAccountIdentity::PrivateAuthorizedUpdate { + 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, @@ -3718,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 @@ -3872,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, @@ -3880,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) @@ -3941,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, @@ -3949,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) @@ -4504,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, @@ -4513,13 +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(), alice_epk_0.clone())], - 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( @@ -4544,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, @@ -4553,13 +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(), alice_epk_1.clone())], - 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( @@ -4587,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 @@ -4600,13 +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(), alice_epk_0)], - 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( @@ -4628,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 @@ -4641,13 +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(), alice_epk_1)], - 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( @@ -4690,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 @@ -4702,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(vec![12_u8; 1088]), - )], - 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 cfbd1703..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,7 +575,7 @@ mod tests { #[test] fn privacy_malicious_programs_cannot_drain_public_victim() { use lee_core::{ - Commitment, InputAccountIdentity, SharedSecretKey, + Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, }; @@ -626,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, @@ -650,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(); @@ -690,7 +727,7 @@ mod tests { #[test] fn privacy_malicious_programs_cannot_drain_private_victim() { use lee_core::{ - Commitment, InputAccountIdentity, SharedSecretKey, + Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey, account::{Account, AccountWithMetadata}, }; @@ -782,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, @@ -807,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(); 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/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/indexer/core/src/block_store.rs b/lez/indexer/core/src/block_store.rs index 878afbe4..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, 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/src/lib.rs b/lez/keycard_wallet/src/lib.rs index 1f009900..73486392 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -7,8 +7,9 @@ use zeroize::Zeroizing; pub mod python_path; -/// NSK and VSK as fixed-length zeroizing byte arrays. -type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>); +/// 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)] @@ -123,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)?; @@ -190,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)?; @@ -239,13 +240,13 @@ impl KeycardWallet { }; let vsk = { - if raw_vsk.len() != 32 { + if raw_vsk.len() != 64 { return Err(PyErr::new::(format!( - "expected 32-byte VSK from keycard, got {} bytes", + "expected 64-byte VSK from keycard, got {} bytes", raw_vsk.len() ))); } - let mut arr = Zeroizing::new([0_u8; 32]); + let mut arr = Zeroizing::new([0_u8; 64]); arr.copy_from_slice(&raw_vsk); arr }; @@ -257,7 +258,7 @@ impl KeycardWallet { 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)?; diff --git a/lez/keycard_wallet/src/python_path.rs b/lez/keycard_wallet/src/python_path.rs index cee051ca..99ed936e 100644 --- a/lez/keycard_wallet/src/python_path.rs +++ b/lez/keycard_wallet/src/python_path.rs @@ -48,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/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/wallet/src/account_manager.rs b/lez/wallet/src/account_manager.rs index 300bbdb1..ce9d1833 100644 --- a/lez/wallet/src/account_manager.rs +++ b/lez/wallet/src/account_manager.rs @@ -8,7 +8,7 @@ use lee_core::{ Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, account::{AccountWithMetadata, Nonce}, - encryption::{EphemeralPublicKey, ViewingPublicKey}, + encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey}, }; use crate::{ExecutionFailureKind, WalletCore}; @@ -169,10 +169,7 @@ impl AccountIdentity { } pub struct PrivateAccountKeys { - pub npk: NullifierPublicKey, pub ssk: SharedSecretKey, - pub vpk: ViewingPublicKey, - pub epk: EphemeralPublicKey, } enum State { @@ -376,12 +373,7 @@ impl AccountManager { 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::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }), State::Public { .. } | State::PublicKeycard { .. } => None, }) .collect() @@ -398,6 +390,8 @@ impl AccountManager { 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, @@ -405,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, @@ -414,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, @@ -421,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, @@ -479,7 +481,7 @@ impl AccountManager { .collect(); if let Some(pin) = self.pin.clone() { - pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> { + pyo3::Python::attach(|py| -> pyo3::PyResult<()> { python_path::add_python_path(py)?; let wallet = KeycardWallet::new(py)?; wallet.connect(py, &pin)?; diff --git a/lez/wallet/src/cli/keycard.rs b/lez/wallet/src/cli/keycard.rs index 1dec07d9..a2eae73c 100644 --- a/lez/wallet/src/cli/keycard.rs +++ b/lez/wallet/src/cli/keycard.rs @@ -39,7 +39,7 @@ impl WalletSubcommand for KeycardSubcommand { ) -> Result { match self { Self::Available => { - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py) .expect("`wallet::keycard::available`: unable to setup python path"); @@ -61,7 +61,7 @@ impl WalletSubcommand for KeycardSubcommand { Self::Connect => { let pin = read_pin()?; - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py) .expect("`wallet::keycard::connect`: unable to setup python path"); @@ -81,7 +81,7 @@ impl WalletSubcommand for KeycardSubcommand { Self::Disconnect => { let pin = read_pin()?; - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py) .expect("`wallet::keycard::disconnect`: unable to setup python path"); @@ -105,7 +105,7 @@ impl WalletSubcommand for KeycardSubcommand { Self::Init => { let pin = read_pin()?; - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py) .expect("`wallet::keycard::init`: unable to setup python path"); @@ -128,7 +128,7 @@ impl WalletSubcommand for KeycardSubcommand { let pin = read_pin()?; let mnemonic = read_mnemonic()?; - Python::with_gil(|py| { + Python::attach(|py| { python_path::add_python_path(py) .expect("`wallet::keycard::load`: unable to setup python path"); diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 0afcda6d..7dece16a 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -587,10 +587,6 @@ impl WalletCore { lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( acc_manager.public_account_ids(), acc_manager.public_account_nonces(), - private_account_keys - .iter() - .map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone())) - .collect(), output, )?; 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/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()); + } +}