Merge branch 'main' into Pravdyvy/programs-elfs-deployments

This commit is contained in:
Pravdyvy 2026-06-15 14:15:33 +03:00
commit 93feaba5de
76 changed files with 1185 additions and 461 deletions

60
Cargo.lock generated
View File

@ -2038,7 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
dependencies = [ dependencies = [
"data-encoding", "data-encoding",
"syn 1.0.109", "syn 2.0.117",
] ]
[[package]] [[package]]
@ -2542,7 +2542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -3917,15 +3917,6 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "inout" name = "inout"
version = "0.1.4" version = "0.1.4"
@ -6198,15 +6189,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "mempool" name = "mempool"
version = "0.1.0" version = "0.1.0"
@ -7524,37 +7506,32 @@ dependencies = [
[[package]] [[package]]
name = "pyo3" name = "pyo3"
version = "0.24.2" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
dependencies = [ dependencies = [
"cfg-if",
"indoc",
"libc", "libc",
"memoffset",
"once_cell", "once_cell",
"portable-atomic", "portable-atomic",
"pyo3-build-config", "pyo3-build-config",
"pyo3-ffi", "pyo3-ffi",
"pyo3-macros", "pyo3-macros",
"unindent",
] ]
[[package]] [[package]]
name = "pyo3-build-config" name = "pyo3-build-config"
version = "0.24.2" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
dependencies = [ dependencies = [
"once_cell",
"target-lexicon", "target-lexicon",
] ]
[[package]] [[package]]
name = "pyo3-ffi" name = "pyo3-ffi"
version = "0.24.2" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
dependencies = [ dependencies = [
"libc", "libc",
"pyo3-build-config", "pyo3-build-config",
@ -7562,9 +7539,9 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros" name = "pyo3-macros"
version = "0.24.2" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"pyo3-macros-backend", "pyo3-macros-backend",
@ -7574,13 +7551,12 @@ dependencies = [
[[package]] [[package]]
name = "pyo3-macros-backend" name = "pyo3-macros-backend"
version = "0.24.2" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"pyo3-build-config",
"quote", "quote",
"syn 2.0.117", "syn 2.0.117",
] ]
@ -8583,7 +8559,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -9630,7 +9606,7 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -10520,12 +10496,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]] [[package]]
name = "unit-prefix" name = "unit-prefix"
version = "0.5.2" version = "0.5.2"
@ -11056,7 +11026,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]

View File

@ -165,7 +165,7 @@ actix-web = { version = "4.13.0", default-features = false, features = [
] } ] }
clap = { version = "4.5.42", features = ["derive", "env"] } clap = { version = "4.5.42", features = ["derive", "env"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } 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" zeroize = "1"
criterion = { version = "0.8", features = ["html_reports"] } criterion = { version = "0.8", features = ["html_reports"] }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -14,21 +14,37 @@ Per-program Risc0 cycle counts, prover wall time, PPE composition cost, and veri
| Profile | release | | Profile | release |
| GPU acceleration | none | | 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) | | Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) | calib_ms | net_ms |
|---|---|---:|---:|---| |---|---|---:|---:|---|---:|---:|
| authenticated_transfer | Initialize | 43,642 | 1 | 18.86 / 19.41 ± 0.48 | | authenticated_transfer | Initialize | 43,818 | 1 | 30.69 / 31.93 ± 1.03 | 1.31 | 0.29 |
| authenticated_transfer | Transfer | 77,095 | 1 | 19.67 / 20.84 ± 1.16 | | authenticated_transfer | Transfer | 79,958 | 1 | 31.02 / 32.35 ± 0.59 | 2.38 | 0.61 |
| token | Burn | 116,546 | 1 | 24.86 / 25.46 ± 0.63 | | token | Burn | 116,546 | 1 | 36.08 / 37.18 ± 0.60 | 3.47 | 5.67 |
| token | Mint | 116,862 | 1 | 24.47 / 25.08 ± 0.42 | | token | Mint | 116,862 | 1 | 35.67 / 37.73 ± 2.54 | 3.48 | 5.26 |
| token | Transfer | 127,726 | 1 | 25.00 / 25.40 ± 0.29 | | token | Transfer | 127,726 | 1 | 35.49 / 36.86 ± 0.90 | 3.81 | 5.08 |
| clock | Tick (no rollups) | 137,022 | 1 | 21.18 / 21.57 ± 0.41 | | clock | Tick (no rollups) | 137,022 | 1 | 32.12 / 33.16 ± 0.89 | 4.08 | 1.72 |
| ata | Create | 175,056 | 1 | 23.64 / 24.94 ± 1.09 | | ata | Create | 174,515 | 1 | 35.41 / 36.49 ± 0.65 | 5.20 | 5.00 |
| amm | SwapExactInput | 508,634 | 1 | 34.21 / 34.77 ± 0.55 | | amm | SwapExactInput | 508,904 | 1 | 46.71 / 48.06 ± 0.86 | 15.17 | 16.30 |
| amm | AddLiquidity | 642,774 | 1 | 37.59 / 37.87 ± 0.28 | | 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` (119 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`) ## Real proving (`--prove`)
@ -85,7 +101,8 @@ The corresponding `proof_bytes` (S_agg) for the bench receipt is captured by `--
## Reproduce ## Reproduce
```sh ```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 prove -- --prove
cargo run --release -p cycle_bench --features ppe -- --prove --ppe cargo run --release -p cycle_bench --features ppe -- --prove --ppe

View File

@ -11,7 +11,7 @@ use lee::{
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
}; };
use lee_core::{ use lee_core::{
InputAccountIdentity, NullifierPublicKey, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
account::AccountWithMetadata, account::AccountWithMetadata,
encryption::{EphemeralPublicKey, ViewingPublicKey}, 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 auth_transfer_program_id = Program::authenticated_transfer_program().id();
let nsk: lee_core::NullifierSecretKey = [3; 32]; let nsk: lee_core::NullifierSecretKey = [3; 32];
let npk = NullifierPublicKey::from(&nsk); 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 ssk = SharedSecretKey([55_u8; 32]);
let _epk = EphemeralPublicKey(vec![55_u8; 1088]); let epk = EphemeralPublicKey(vec![55_u8; 1088]);
let attacker_vault_id = { let attacker_vault_id = {
let seed = vault_core::compute_vault_seed(attacker_id); let seed = vault_core::compute_vault_seed(attacker_id);
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337) AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
@ -712,6 +712,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
npk, npk,
ssk, ssk,
identifier: 1337, identifier: 1337,

View File

@ -4,12 +4,14 @@
reason = "We don't care about these in tests" 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 anyhow::Context as _;
use borsh::BorshSerialize; use borsh::BorshSerialize;
use common::transaction::LeeTransaction; 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::{ use lee::{
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program, AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
public_transaction, 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( let message = privacy_preserving_transaction::Message::try_from_circuit_output(
vec![bridge_account_id, recipient_vault_id], vec![bridge_account_id, recipient_vault_id],
vec![bridge_pre.account.nonce, vault_pre.account.nonce], vec![bridge_pre.account.nonce, vault_pre.account.nonce],
vec![],
output, output,
) )
.context("Failed to build privacy-preserving bridge deposit message")?; .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" "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(()) Ok(())
} }

View File

@ -23,7 +23,7 @@ use lee::{
program::Program, program::Program,
}; };
use lee_core::{ use lee_core::{
InputAccountIdentity, NullifierPublicKey, EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
encryption::ViewingPublicKey, encryption::ViewingPublicKey,
program::PdaSeed, program::PdaSeed,
@ -74,6 +74,8 @@ async fn fund_private_pda(
let account_identities = vec![ let account_identities = vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
npk, npk,
ssk, ssk,
identifier, identifier,
@ -89,13 +91,9 @@ async fn fund_private_pda(
) )
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?; .map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
let message = Message::try_from_circuit_output( let message =
vec![sender], Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output)
vec![sender_account.nonce], .map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
vec![(npk, vpk, epk)],
output,
)
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]); let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);

View File

@ -23,7 +23,7 @@ use lee::{
public_transaction as putx, public_transaction as putx,
}; };
use lee_core::{ use lee_core::{
InputAccountIdentity, MembershipProof, NullifierPublicKey, EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
account::{AccountWithMetadata, Nonce, data::Data}, account::{AccountWithMetadata, Nonce, data::Data},
encryption::ViewingPublicKey, encryption::ViewingPublicKey,
}; };
@ -301,12 +301,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: sender_epk,
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
ssk: sender_ss, ssk: sender_ss,
nsk: sender_nsk, nsk: sender_nsk,
membership_proof: proof, membership_proof: proof,
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: recipient_epk,
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
npk: recipient_npk, npk: recipient_npk,
ssk: recipient_ss, ssk: recipient_ss,
identifier: 0, identifier: 0,
@ -315,16 +319,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
&program.into(), &program.into(),
) )
.unwrap(); .unwrap();
let message = pptx::message::Message::try_from_circuit_output( let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![
(sender_npk, sender_vpk, sender_epk),
(recipient_npk, recipient_vpk, recipient_epk),
],
output,
)
.unwrap();
let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]); let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
pptx::PrivacyPreservingTransaction::new(message, witness_set) pptx::PrivacyPreservingTransaction::new(message, witness_set)
} }

View File

@ -1,4 +1,4 @@
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; use k256::elliptic_curve::PrimeField as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::key_management::key_tree::traits::KeyTreeNode; 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)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] #[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
pub struct ChildKeysPublic { pub struct ChildKeysPublic {
pub csk: lee::PrivateKey, /// Secret key for public account.
pub cpk: lee::PublicKey, pub sk: lee::PrivateKey,
pub ccc: [u8; 32], /// Schnorr secret key.
pub ssk: lee::PrivateKey,
/// Schnorr public key.
pub pk: lee::PublicKey,
pub cc: [u8; 32],
/// Can be [`None`] if root. /// Can be [`None`] if root.
pub cci: Option<u32>, pub cci: Option<u32>,
} }
@ -18,19 +22,24 @@ impl ChildKeysPublic {
pub fn root(seed: [u8; 64]) -> Self { pub fn root(seed: [u8; 64]) -> Self {
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); 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 *hash_value
.first_chunk::<32>() .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"),
) )
.expect("Expect a valid Private Key"); .expect("Expect a valid Private Key");
let ccc = *hash_value.last_chunk::<32>().unwrap(); let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`");
let cpk = lee::PublicKey::new_from_private_key(&csk);
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 { Self {
csk, sk,
cpk, ssk,
ccc, pk,
cc,
cci: None, cci: None,
} }
} }
@ -39,61 +48,53 @@ impl ChildKeysPublic {
pub fn nth_child(&self, cci: u32) -> Self { pub fn nth_child(&self, cci: u32) -> Self {
let hash_value = self.compute_hash_value(cci); let hash_value = self.compute_hash_value(cci);
let csk = lee::PrivateKey::try_new({ let lhs = k256::Scalar::from_repr(
let hash_value = hash_value (*hash_value
.first_chunk::<32>() .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 = let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into())
k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar"); .expect("Expect a valid private key");
let value_2 = k256::Scalar::from_repr((*self.csk.value()).into())
.expect("Expect a valid k256 scalar");
let sum = value_1.add(&value_2); let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`");
sum.to_bytes().into()
})
.expect("Expect a valid private key");
let ccc = *hash_value let cc = *hash_value
.last_chunk::<32>() .last_chunk::<32>()
.expect("hash_value is 64 bytes, must be safe to get last 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 { Self {
csk, sk,
cpk, ssk,
ccc, pk,
cc,
cci: Some(cci), cci: Some(cci),
} }
} }
#[must_use] #[must_use]
pub fn account_id(&self) -> lee::AccountId { 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] { fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
let mut hash_input = vec![]; let mut hash_input = vec![];
// Simplified key logic by only supporting harden keys.
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater { // Non-harden keys would require access to untweaked public keys associated to `sk`s.
// Non-harden. // Thus, not PQ secure.
// BIP-032 compatibility requires 1-byte header from the public_key; hash_input.extend_from_slice(&[0_u8]);
// Not stored in `self.cpk.value()`. hash_input.extend_from_slice(self.sk.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());
}
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
hash_input.extend_from_slice(&cci.to_be_bytes()); 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 { impl<'a> From<&'a ChildKeysPublic> for &'a lee::PrivateKey {
fn from(value: &'a ChildKeysPublic) -> Self { fn from(value: &'a ChildKeysPublic) -> Self {
&value.csk &value.ssk
} }
} }
@ -137,30 +138,37 @@ mod tests {
]; ];
let keys = ChildKeysPublic::root(seed); 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, 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, 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, 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, 202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86,
]) ])
.unwrap(); .unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([ let expected_ssk: PrivateKey = PrivateKey::try_new([
219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247, 207, 4, 246, 223, 104, 72, 19, 85, 14, 122, 194, 82, 32, 163, 60, 57, 8, 25, 209, 91,
244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245, 254, 107, 76, 238, 31, 68, 236, 192, 154, 78, 105, 118,
]) ])
.unwrap(); .unwrap();
assert!(expected_ccc == keys.ccc); let expected_pk: PublicKey = PublicKey::try_new([
assert!(expected_csk == keys.csk); 188, 163, 203, 45, 151, 154, 230, 254, 123, 114, 158, 130, 19, 182, 164, 143, 150, 131,
assert!(expected_cpk == keys.cpk); 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] #[test]
fn harden_child_keys_generation() { fn child_keys_generation() {
let seed = [ let seed = [
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, 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, 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 cci = (2_u32).pow(31) + 13;
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); 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, 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, 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, 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, 213, 33, 52, 188, 22, 198, 246, 71, 46, 245, 174, 16, 47,
]) ])
.unwrap(); .unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([ let expected_ssk: PrivateKey = PrivateKey::try_new([
142, 143, 238, 159, 105, 165, 224, 252, 108, 62, 53, 209, 176, 219, 249, 38, 90, 241, 100, 37, 212, 81, 40, 233, 72, 156, 177, 139, 50, 114, 136, 157, 202, 132, 203, 246,
201, 81, 194, 146, 236, 5, 83, 152, 238, 243, 138, 16, 229, 15, 252, 242, 13, 81, 42, 100, 159, 240, 187, 252, 202, 108, 25, 105,
]) ])
.unwrap(); .unwrap();
assert!(expected_ccc == child_keys.ccc); let expected_pk: PublicKey = PublicKey::try_new([
assert!(expected_csk == child_keys.csk); 210, 59, 119, 137, 21, 153, 82, 22, 195, 82, 12, 16, 80, 156, 125, 199, 19, 173, 46,
assert!(expected_cpk == child_keys.cpk); 224, 213, 144, 165, 126, 70, 129, 171, 141, 77, 212, 108, 233,
}
#[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,
]) ])
.unwrap(); .unwrap();
let expected_cpk: PublicKey = PublicKey::try_new([ assert!(expected_cc == child_keys.cc);
119, 16, 145, 121, 97, 244, 186, 35, 136, 34, 140, 171, 206, 139, 11, 208, 207, 121, assert!(expected_ssk == child_keys.ssk);
158, 45, 28, 22, 140, 98, 161, 179, 212, 173, 238, 220, 2, 34, assert!(expected_sk == child_keys.sk);
]) assert!(expected_pk == child_keys.pk);
.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);
} }
} }

View File

@ -347,8 +347,8 @@ mod tests {
assert!(tree.key_map.contains_key(&ChainIndex::root())); assert!(tree.key_map.contains_key(&ChainIndex::root()));
assert!(tree.account_id_map.contains_key(&AccountId::new([ 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, 10, 231, 159, 65, 236, 46, 205, 5, 172, 89, 250, 29, 123, 195, 212, 137, 155, 111, 40,
38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247 120, 53, 28, 124, 54, 224, 170, 119, 208, 2, 72, 75, 50
]))); ])));
} }

View File

@ -4,7 +4,7 @@ use crate::{
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey, Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
NullifierSecretKey, SharedSecretKey, NullifierSecretKey, SharedSecretKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
encryption::Ciphertext, encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow}, program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
}; };
@ -33,6 +33,8 @@ pub enum InputAccountIdentity {
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and /// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
/// matched against `pre_state.account_id`. /// matched against `pre_state.account_id`.
PrivateAuthorizedInit { PrivateAuthorizedInit {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey, ssk: SharedSecretKey,
nsk: NullifierSecretKey, nsk: NullifierSecretKey,
identifier: Identifier, identifier: Identifier,
@ -40,6 +42,8 @@ pub enum InputAccountIdentity {
/// Update of an authorized standalone private account: existing on-chain commitment, with /// Update of an authorized standalone private account: existing on-chain commitment, with
/// membership proof. /// membership proof.
PrivateAuthorizedUpdate { PrivateAuthorizedUpdate {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey, ssk: SharedSecretKey,
nsk: NullifierSecretKey, nsk: NullifierSecretKey,
membership_proof: MembershipProof, 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 /// 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. /// doesn't yet exist on chain). No `nsk`, no membership proof.
PrivateUnauthorized { PrivateUnauthorized {
epk: EphemeralPublicKey,
view_tag: ViewTag,
npk: NullifierPublicKey, npk: NullifierPublicKey,
ssk: SharedSecretKey, ssk: SharedSecretKey,
identifier: Identifier, identifier: Identifier,
@ -57,6 +63,8 @@ pub enum InputAccountIdentity {
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it /// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
/// as the 4th input. /// as the 4th input.
PrivatePdaInit { PrivatePdaInit {
epk: EphemeralPublicKey,
view_tag: ViewTag,
npk: NullifierPublicKey, npk: NullifierPublicKey,
ssk: SharedSecretKey, ssk: SharedSecretKey,
identifier: Identifier, identifier: Identifier,
@ -72,6 +80,8 @@ pub enum InputAccountIdentity {
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a /// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
/// previously-seen authorization in a chained call. /// previously-seen authorization in a chained call.
PrivatePdaUpdate { PrivatePdaUpdate {
epk: EphemeralPublicKey,
view_tag: ViewTag,
ssk: SharedSecretKey, ssk: SharedSecretKey,
nsk: NullifierSecretKey, nsk: NullifierSecretKey,
membership_proof: MembershipProof, membership_proof: MembershipProof,
@ -123,7 +133,7 @@ impl InputAccountIdentity {
pub struct PrivacyPreservingCircuitOutput { pub struct PrivacyPreservingCircuitOutput {
pub public_pre_states: Vec<AccountWithMetadata>, pub public_pre_states: Vec<AccountWithMetadata>,
pub public_post_states: Vec<Account>, pub public_post_states: Vec<Account>,
pub ciphertexts: Vec<Ciphertext>, pub encrypted_private_post_states: Vec<EncryptedAccountData>,
pub new_commitments: Vec<Commitment>, pub new_commitments: Vec<Commitment>,
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>, pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
pub block_validity_window: BlockValidityWindow, pub block_validity_window: BlockValidityWindow,
@ -148,6 +158,7 @@ mod tests {
use crate::{ use crate::{
Commitment, Nullifier, Commitment, Nullifier,
account::{Account, AccountId, AccountWithMetadata, Nonce}, account::{Account, AccountId, AccountWithMetadata, Nonce},
encryption::Ciphertext,
}; };
#[test] #[test]
@ -181,7 +192,11 @@ mod tests {
data: b"post state data".to_vec().try_into().unwrap(), data: b"post state data".to_vec().try_into().unwrap(),
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF), 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( new_commitments: vec![Commitment::new(
&AccountId::new([1; 32]), &AccountId::new([1; 32]),
&Account::default(), &Account::default(),

View File

@ -6,7 +6,7 @@ use chacha20::{
use risc0_zkvm::sha::{Impl, Sha256 as _}; use risc0_zkvm::sha::{Impl, Sha256 as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "host")] #[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}; use crate::{Commitment, account::Account, program::PrivateAccountKind};
#[cfg(feature = "host")] #[cfg(feature = "host")]
@ -17,6 +17,11 @@ pub type Scalar = [u8; 32];
#[derive(Serialize, Deserialize, Clone, Copy)] #[derive(Serialize, Deserialize, Clone, Copy)]
pub struct SharedSecretKey(pub [u8; 32]); 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<u8>);
pub struct EncryptionScheme; pub struct EncryptionScheme;
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] #[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 { impl EncryptionScheme {
#[must_use] #[must_use]
pub fn encrypt( pub fn encrypt(

View File

@ -2,12 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed}; use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::SharedSecretKey; use crate::{EphemeralPublicKey, 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<u8>);
/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate). /// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate).
#[derive( #[derive(

View File

@ -10,7 +10,9 @@ pub use commitment::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
compute_digest_for_path, 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 nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
pub use program::PrivateAccountKind; pub use program::PrivateAccountKind;

View File

@ -178,8 +178,8 @@ mod tests {
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")] #![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
use lee_core::{ use lee_core::{
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
PrivacyPreservingCircuitOutput, SharedSecretKey, EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
program::{PdaSeed, PrivateAccountKind}, program::{PdaSeed, PrivateAccountKind},
}; };
@ -201,7 +201,7 @@ mod tests {
idx: usize, idx: usize,
) -> PrivateAccountKind { ) -> PrivateAccountKind {
let (kind, _) = EncryptionScheme::decrypt( let (kind, _) = EncryptionScheme::decrypt(
&output.ciphertexts[idx], &output.encrypted_private_post_states[idx].ciphertext,
ssk, ssk,
&output.new_commitments[idx], &output.new_commitments[idx],
u32::try_from(idx).expect("idx fits in u32"), u32::try_from(idx).expect("idx fits in u32"),
@ -210,6 +210,17 @@ mod tests {
kind 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] #[test]
fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() { fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() {
let recipient_keys = test_private_account_keys_1(); let recipient_keys = test_private_account_keys_1();
@ -257,6 +268,11 @@ mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -274,10 +290,10 @@ mod tests {
assert_eq!(sender_post, expected_sender_post); assert_eq!(sender_post, expected_sender_post);
assert_eq!(output.new_commitments.len(), 1); assert_eq!(output.new_commitments.len(), 1);
assert_eq!(output.new_nullifiers.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( let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0], &output.encrypted_private_post_states[0].ciphertext,
&shared_secret, &shared_secret,
&output.new_commitments[0], &output.new_commitments[0],
0, 0,
@ -356,6 +372,11 @@ mod tests {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret_1, ssk: shared_secret_1,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: commitment_set membership_proof: commitment_set
@ -364,6 +385,11 @@ mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: shared_secret_2, ssk: shared_secret_2,
identifier: 0, identifier: 0,
@ -378,10 +404,10 @@ mod tests {
assert!(output.public_post_states.is_empty()); assert!(output.public_post_states.is_empty());
assert_eq!(output.new_commitments, expected_new_commitments); assert_eq!(output.new_commitments, expected_new_commitments);
assert_eq!(output.new_nullifiers, expected_new_nullifiers); 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( let (_identifier, sender_post) = EncryptionScheme::decrypt(
&output.ciphertexts[0], &output.encrypted_private_post_states[0].ciphertext,
&shared_secret_1, &shared_secret_1,
&expected_new_commitments[0], &expected_new_commitments[0],
0, 0,
@ -390,7 +416,7 @@ mod tests {
assert_eq!(sender_post, expected_private_account_1); assert_eq!(sender_post, expected_private_account_1);
let (_identifier, recipient_post) = EncryptionScheme::decrypt( let (_identifier, recipient_post) = EncryptionScheme::decrypt(
&output.ciphertexts[1], &output.encrypted_private_post_states[1].ciphertext,
&shared_secret_2, &shared_secret_2,
&expected_new_commitments[1], &expected_new_commitments[1],
1, 1,
@ -432,6 +458,11 @@ mod tests {
vec![pre], vec![pre],
instruction, instruction,
vec![InputAccountIdentity::PrivateUnauthorized { vec![InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
npk: account_keys.npk(), npk: account_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -461,6 +492,8 @@ mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction(seed).unwrap(), Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier, identifier,
@ -508,6 +541,8 @@ mod tests {
vec![pda_pre], vec![pda_pre],
instruction, instruction,
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret_pda, ssk: shared_secret_pda,
identifier: 0, identifier: 0,
@ -561,6 +596,8 @@ mod tests {
instruction, instruction,
vec![ vec![
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret_pda, ssk: shared_secret_pda,
identifier: 0, identifier: 0,
@ -618,6 +655,11 @@ mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&shared_npk,
&shared_keys.vpk(),
),
npk: shared_npk, npk: shared_npk,
ssk: shared_secret, ssk: shared_secret,
identifier: shared_identifier, identifier: shared_identifier,
@ -647,6 +689,8 @@ mod tests {
Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize) Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize)
.unwrap(), .unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit { vec![InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
ssk, ssk,
nsk: keys.nsk, nsk: keys.nsk,
identifier, identifier,
@ -691,6 +735,8 @@ mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
npk: keys.npk(), npk: keys.npk(),
ssk, ssk,
identifier, identifier,
@ -735,6 +781,8 @@ mod tests {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
ssk, ssk,
nsk: keys.nsk, nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(), 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(), Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
vec![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
ssk, ssk,
nsk: keys.nsk, nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
@ -827,6 +877,8 @@ mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction(seed).unwrap(), Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: 99, identifier: 99,
@ -870,6 +922,8 @@ mod tests {
Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(), Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
vec![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
ssk, ssk,
nsk: keys.nsk, nsk: keys.nsk,
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(), membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),

View File

@ -1,52 +1,16 @@
use borsh::{BorshDeserialize, BorshSerialize}; use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{ use lee_core::{
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput, Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
account::{Account, Nonce}, account::{Account, Nonce},
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
program::{BlockValidityWindow, TimestampValidityWindow}, program::{BlockValidityWindow, TimestampValidityWindow},
}; };
pub use lee_core::{EncryptedAccountData, ViewTag};
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
use crate::{AccountId, error::LeeError}; use crate::{AccountId, error::LeeError};
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00"; 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)] #[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub struct Message { pub struct Message {
pub public_account_ids: Vec<AccountId>, pub public_account_ids: Vec<AccountId>,
@ -92,28 +56,13 @@ impl Message {
pub fn try_from_circuit_output( pub fn try_from_circuit_output(
public_account_ids: Vec<AccountId>, public_account_ids: Vec<AccountId>,
nonces: Vec<Nonce>, nonces: Vec<Nonce>,
public_keys: Vec<(NullifierPublicKey, ViewingPublicKey, EphemeralPublicKey)>,
output: PrivacyPreservingCircuitOutput, output: PrivacyPreservingCircuitOutput,
) -> Result<Self, LeeError> { ) -> Result<Self, LeeError> {
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 { Ok(Self {
public_account_ids, public_account_ids,
nonces, nonces,
public_post_states: output.public_post_states, 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_commitments: output.new_commitments,
new_nullifiers: output.new_nullifiers, new_nullifiers: output.new_nullifiers,
block_validity_window: output.block_validity_window, block_validity_window: output.block_validity_window,

View File

@ -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] #[test]
fn program_execution() { fn program_execution() {
let program = Program::simple_balance_transfer(); let program = Program::simple_balance_transfer();

View File

@ -16,3 +16,18 @@ impl Message {
self.bytecode 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]);
}
}

View File

@ -1,7 +1,9 @@
use std::str::FromStr; use std::str::FromStr;
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
use rand::{Rng as _, rngs::OsRng}; use rand::{Rng as _, rngs::OsRng};
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
use sha2::{Digest as _, Sha256};
use crate::error::LeeError; use crate::error::LeeError;
@ -60,6 +62,29 @@ impl PrivateKey {
pub const fn value(&self) -> &[u8; 32] { pub const fn value(&self) -> &[u8; 32] {
&self.0 &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<Self, LeeError> {
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)] #[cfg(test)]
@ -75,4 +100,33 @@ mod tests {
fn produce_key() { fn produce_key() {
let _key = PrivateKey::new_os_random(); 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
]
);
}
} }

View File

@ -418,8 +418,8 @@ pub mod tests {
use authenticated_transfer_core::Instruction as AuthTransferInstruction; use authenticated_transfer_core::Instruction as AuthTransferInstruction;
use lee_core::{ use lee_core::{
BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey, BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
NullifierSecretKey, SharedSecretKey, Timestamp, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data}, account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
encryption::{EphemeralPublicKey, ViewingPublicKey}, encryption::{EphemeralPublicKey, ViewingPublicKey},
program::{ program::{
@ -613,6 +613,48 @@ pub mod tests {
PublicTransaction::new(message, witness_set) 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] #[test]
fn new_with_genesis() { fn new_with_genesis() {
let key1 = PrivateKey::try_new([1; 32]).unwrap(); let key1 = PrivateKey::try_new([1; 32]).unwrap();
@ -1376,6 +1418,11 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -1388,7 +1435,6 @@ pub mod tests {
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(
vec![sender_keys.account_id()], vec![sender_keys.account_id()],
vec![sender_nonce], vec![sender_nonce],
vec![(recipient_keys.npk(), recipient_keys.vpk(), epk)],
output, output,
) )
.unwrap(); .unwrap();
@ -1429,6 +1475,11 @@ pub mod tests {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: epk_1,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret_1, ssk: shared_secret_1,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: state membership_proof: state
@ -1437,6 +1488,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: epk_2,
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: shared_secret_2, ssk: shared_secret_2,
identifier: 0, identifier: 0,
@ -1446,16 +1502,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![
(sender_keys.npk(), sender_keys.vpk(), epk_1),
(recipient_keys.npk(), recipient_keys.vpk(), epk_2),
],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
@ -1494,6 +1541,11 @@ pub mod tests {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: state membership_proof: state
@ -1507,13 +1559,8 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![*recipient_account_id], Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
vec![],
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
@ -1630,6 +1677,79 @@ pub mod tests {
assert!(state.private_state.1.contains(&expected_new_nullifier)); 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] #[test]
fn transition_from_privacy_preserving_transaction_deshielded() { fn transition_from_privacy_preserving_transaction_deshielded() {
let sender_keys = test_private_account_keys_1(); let sender_keys = test_private_account_keys_1();
@ -1992,6 +2112,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2003,6 +2128,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2048,6 +2178,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2059,6 +2194,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2104,6 +2244,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2115,6 +2260,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2160,6 +2310,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2171,6 +2326,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2216,6 +2376,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2227,6 +2392,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2270,6 +2440,11 @@ pub mod tests {
Program::serialize_instruction(10_u128).unwrap(), Program::serialize_instruction(10_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&sender_keys.vpk(), &sender_keys.vpk(),
&[0_u8; 32], &[0_u8; 32],
@ -2281,6 +2456,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
npk: recipient_keys.npk(), npk: recipient_keys.npk(),
ssk: SharedSecretKey::encapsulate_deterministic( ssk: SharedSecretKey::encapsulate_deterministic(
&recipient_keys.vpk(), &recipient_keys.vpk(),
@ -2326,6 +2506,8 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2359,6 +2541,8 @@ pub mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction(seed).unwrap(), Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2370,7 +2554,7 @@ pub mod tests {
let (output, _proof) = result.expect("private PDA claim should succeed"); let (output, _proof) = result.expect("private PDA claim should succeed");
assert_eq!(output.new_nullifiers.len(), 1); assert_eq!(output.new_nullifiers.len(), 1);
assert_eq!(output.new_commitments.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_pre_states.is_empty());
assert!(output.public_post_states.is_empty()); assert!(output.public_post_states.is_empty());
} }
@ -2400,6 +2584,8 @@ pub mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction(seed).unwrap(), Program::serialize_instruction(seed).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()),
npk: npk_b, npk: npk_b,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2437,6 +2623,8 @@ pub mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction((seed, seed, callee_id)).unwrap(), Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2477,6 +2665,8 @@ pub mod tests {
vec![pre_state], vec![pre_state],
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(), Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2516,12 +2706,16 @@ pub mod tests {
Program::serialize_instruction(seed).unwrap(), Program::serialize_instruction(seed).unwrap(),
vec![ vec![
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()),
npk: keys_a.npk(), npk: keys_a.npk(),
ssk: shared_a, ssk: shared_a,
identifier: u128::MAX, identifier: u128::MAX,
seed: None, seed: None,
}, },
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()),
npk: keys_b.npk(), npk: keys_b.npk(),
ssk: shared_b, ssk: shared_b,
identifier: u128::MAX, identifier: u128::MAX,
@ -2564,6 +2758,8 @@ pub mod tests {
vec![owned_pre_state], vec![owned_pre_state],
Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivatePdaInit { vec![InputAccountIdentity::PrivatePdaInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
npk, npk,
ssk: shared_secret, ssk: shared_secret,
identifier: u128::MAX, identifier: u128::MAX,
@ -2652,12 +2848,22 @@ pub mod tests {
Program::serialize_instruction(100_u128).unwrap(), Program::serialize_instruction(100_u128).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: (1, vec![]), membership_proof: (1, vec![]),
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: (1, vec![]), membership_proof: (1, vec![]),
@ -3003,6 +3209,11 @@ pub mod tests {
.unwrap(), .unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&sender_keys.npk(),
&sender_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
membership_proof: state membership_proof: state
@ -3016,13 +3227,9 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![recipient_account_id], Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output)
vec![Nonce(0)], .unwrap();
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -3129,6 +3336,11 @@ pub mod tests {
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![ vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: to_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&to_keys.npk(),
&to_keys.vpk(),
),
ssk: to_ss, ssk: to_ss,
nsk: from_keys.nsk, nsk: from_keys.nsk,
membership_proof: state membership_proof: state
@ -3137,6 +3349,11 @@ pub mod tests {
identifier: 0, identifier: 0,
}, },
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: from_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&from_keys.npk(),
&from_keys.vpk(),
),
ssk: from_ss, ssk: from_ss,
nsk: to_keys.nsk, nsk: to_keys.nsk,
membership_proof: state membership_proof: state
@ -3149,16 +3366,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![
(to_keys.npk(), to_keys.vpk(), to_epk),
(from_keys.npk(), from_keys.vpk(), from_epk),
],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
let transaction = PrivacyPreservingTransaction::new(message, witness_set); let transaction = PrivacyPreservingTransaction::new(message, witness_set);
@ -3406,6 +3614,11 @@ pub mod tests {
vec![authorized_account], vec![authorized_account],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit { vec![InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: private_keys.nsk, nsk: private_keys.nsk,
identifier: 0, identifier: 0,
@ -3415,13 +3628,7 @@ pub mod tests {
.unwrap(); .unwrap();
// Create message from circuit output // Create message from circuit output
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![(private_keys.npk(), private_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
@ -3454,6 +3661,11 @@ pub mod tests {
vec![unauthorized_account], vec![unauthorized_account],
Program::serialize_instruction(0_u128).unwrap(), Program::serialize_instruction(0_u128).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized { vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
npk: private_keys.npk(), npk: private_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -3462,13 +3674,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![(private_keys.npk(), private_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -3506,6 +3712,11 @@ pub mod tests {
vec![authorized_account.clone()], vec![authorized_account.clone()],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit { vec![InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret, ssk: shared_secret,
nsk: private_keys.nsk, nsk: private_keys.nsk,
identifier: 0, identifier: 0,
@ -3514,13 +3725,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![(private_keys.npk(), private_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
let tx = PrivacyPreservingTransaction::new(message, witness_set); let tx = PrivacyPreservingTransaction::new(message, witness_set);
@ -3553,6 +3758,11 @@ pub mod tests {
vec![account_metadata], vec![account_metadata],
Program::serialize_instruction(()).unwrap(), Program::serialize_instruction(()).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedInit { vec![InputAccountIdentity::PrivateAuthorizedInit {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&private_keys.npk(),
&private_keys.vpk(),
),
ssk: shared_secret2, ssk: shared_secret2,
nsk: private_keys.nsk, nsk: private_keys.nsk,
identifier: 0, identifier: 0,
@ -3630,6 +3840,11 @@ pub mod tests {
vec![private_account], vec![private_account],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate { 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) ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0, .0,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
@ -3657,6 +3872,11 @@ pub mod tests {
vec![private_account], vec![private_account],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateAuthorizedUpdate { 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) ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
.0, .0,
nsk: sender_keys.nsk, nsk: sender_keys.nsk,
@ -3718,6 +3938,11 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: EphemeralPublicKey(Vec::new()),
view_tag: EncryptedAccountData::compute_view_tag(
&recipient_keys.npk(),
&recipient_keys.vpk(),
),
ssk: recipient, ssk: recipient,
nsk: recipient_keys.nsk, nsk: recipient_keys.nsk,
membership_proof: state membership_proof: state
@ -3872,6 +4097,11 @@ pub mod tests {
vec![pre], vec![pre],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized { vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
npk: account_keys.npk(), npk: account_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -3880,13 +4110,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![(account_keys.npk(), account_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set) PrivacyPreservingTransaction::new(message, witness_set)
@ -3941,6 +4165,11 @@ pub mod tests {
vec![pre], vec![pre],
Program::serialize_instruction(instruction).unwrap(), Program::serialize_instruction(instruction).unwrap(),
vec![InputAccountIdentity::PrivateUnauthorized { vec![InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag: EncryptedAccountData::compute_view_tag(
&account_keys.npk(),
&account_keys.vpk(),
),
npk: account_keys.npk(), npk: account_keys.npk(),
ssk: shared_secret, ssk: shared_secret,
identifier: 0, identifier: 0,
@ -3949,13 +4178,7 @@ pub mod tests {
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
vec![],
vec![],
vec![(account_keys.npk(), account_keys.vpk(), epk)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
PrivacyPreservingTransaction::new(message, witness_set) PrivacyPreservingTransaction::new(message, witness_set)
@ -4504,6 +4727,11 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: alice_epk_0.clone(),
view_tag: EncryptedAccountData::compute_view_tag(
&alice_npk,
&alice_keys.vpk(),
),
npk: alice_npk, npk: alice_npk,
ssk: alice_shared_0, ssk: alice_shared_0,
identifier: 0, identifier: 0,
@ -4513,13 +4741,9 @@ pub mod tests {
&auth_transfer.clone().into(), &auth_transfer.clone().into(),
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![funder_id], Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
vec![funder_nonce], .unwrap();
vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -4544,6 +4768,11 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk: alice_epk_1.clone(),
view_tag: EncryptedAccountData::compute_view_tag(
&alice_npk,
&alice_keys.vpk(),
),
npk: alice_npk, npk: alice_npk,
ssk: alice_shared_1, ssk: alice_shared_1,
identifier: 1, identifier: 1,
@ -4553,13 +4782,9 @@ pub mod tests {
&auth_transfer.into(), &auth_transfer.into(),
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![funder_id], Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
vec![funder_nonce], .unwrap();
vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -4587,6 +4812,11 @@ pub mod tests {
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
vec![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk: alice_epk_0,
view_tag: EncryptedAccountData::compute_view_tag(
&alice_npk,
&alice_keys.vpk(),
),
ssk: alice_shared_0, ssk: alice_shared_0,
nsk: alice_keys.nsk, nsk: alice_keys.nsk,
membership_proof: state membership_proof: state
@ -4600,13 +4830,9 @@ pub mod tests {
&spend_with_deps, &spend_with_deps,
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![recipient_id], Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output)
vec![Nonce(0)], .unwrap();
vec![(alice_npk, alice_keys.vpk(), alice_epk_0)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -4628,6 +4854,11 @@ pub mod tests {
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(), Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
vec![ vec![
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk: alice_epk_1,
view_tag: EncryptedAccountData::compute_view_tag(
&alice_npk,
&alice_keys.vpk(),
),
ssk: alice_shared_1, ssk: alice_shared_1,
nsk: alice_keys.nsk, nsk: alice_keys.nsk,
membership_proof: state membership_proof: state
@ -4641,13 +4872,8 @@ pub mod tests {
&spend_with_deps, &spend_with_deps,
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![recipient_id], Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap();
vec![],
vec![(alice_npk, alice_keys.vpk(), alice_epk_1)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[]); let witness_set = WitnessSet::for_message(&message, proof, &[]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(
@ -4690,6 +4916,11 @@ pub mod tests {
vec![ vec![
InputAccountIdentity::Public, InputAccountIdentity::Public,
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk: EphemeralPublicKey(vec![12_u8; 1088]),
view_tag: EncryptedAccountData::compute_view_tag(
&alice_npk,
&alice_keys.vpk(),
),
nsk: alice_keys.nsk, nsk: alice_keys.nsk,
ssk: alice_shared_1_refund, ssk: alice_shared_1_refund,
membership_proof: state membership_proof: state
@ -4702,17 +4933,9 @@ pub mod tests {
&Program::authenticated_transfer_program().into(), &Program::authenticated_transfer_program().into(),
) )
.unwrap(); .unwrap();
let message = Message::try_from_circuit_output( let message =
vec![recipient_id], Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output)
vec![recipient_nonce], .unwrap();
vec![(
alice_npk,
alice_keys.vpk(),
EphemeralPublicKey(vec![12_u8; 1088]),
)],
output,
)
.unwrap();
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]); let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
state state
.transition_from_privacy_preserving_transaction( .transition_from_privacy_preserving_transaction(

View File

@ -492,12 +492,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
let output = PrivacyPreservingCircuitOutput { let output = PrivacyPreservingCircuitOutput {
public_pre_states: public_pre_states.to_vec(), public_pre_states: public_pre_states.to_vec(),
public_post_states: message.public_post_states.clone(), public_post_states: message.public_post_states.clone(),
ciphertexts: message encrypted_private_post_states: message.encrypted_private_post_states.clone(),
.encrypted_private_post_states
.iter()
.cloned()
.map(|value| value.ciphertext)
.collect(),
new_commitments: message.new_commitments.clone(), new_commitments: message.new_commitments.clone(),
new_nullifiers: message.new_nullifiers.clone(), new_nullifiers: message.new_nullifiers.clone(),
block_validity_window: message.block_validity_window, block_validity_window: message.block_validity_window,
@ -526,6 +521,44 @@ mod tests {
validated_state_diff::ValidatedStateDiff, 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 /// Privacy-path version of the authorization-injection attack. The test passes when the
/// attack is rejected and the victim's balance is left untouched. /// attack is rejected and the victim's balance is left untouched.
/// ///
@ -542,7 +575,7 @@ mod tests {
#[test] #[test]
fn privacy_malicious_programs_cannot_drain_public_victim() { fn privacy_malicious_programs_cannot_drain_public_victim() {
use lee_core::{ use lee_core::{
Commitment, InputAccountIdentity, SharedSecretKey, Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
}; };
@ -626,6 +659,11 @@ mod tests {
// [2] recipient — first seen in authenticated_transfer's program_output.pre_states // [2] recipient — first seen in authenticated_transfer's program_output.pre_states
let account_identities = vec![ let account_identities = vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: attacker_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&attacker_keys.npk(),
&attacker_keys.vpk(),
),
ssk: attacker_ssk, ssk: attacker_ssk,
nsk: attacker_keys.nsk, nsk: attacker_keys.nsk,
membership_proof, membership_proof,
@ -650,7 +688,6 @@ mod tests {
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(
vec![victim_id, recipient_id], vec![victim_id, recipient_id],
vec![], // no public signers, no nonces vec![], // no public signers, no nonces
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
circuit_output, circuit_output,
) )
.unwrap(); .unwrap();
@ -690,7 +727,7 @@ mod tests {
#[test] #[test]
fn privacy_malicious_programs_cannot_drain_private_victim() { fn privacy_malicious_programs_cannot_drain_private_victim() {
use lee_core::{ use lee_core::{
Commitment, InputAccountIdentity, SharedSecretKey, Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
account::{Account, AccountWithMetadata}, account::{Account, AccountWithMetadata},
}; };
@ -782,6 +819,11 @@ mod tests {
// so PrivateAuthorizedUpdate is not an option. // so PrivateAuthorizedUpdate is not an option.
let account_identities = vec![ let account_identities = vec![
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: attacker_epk,
view_tag: EncryptedAccountData::compute_view_tag(
&attacker_keys.npk(),
&attacker_keys.vpk(),
),
ssk: attacker_ssk, ssk: attacker_ssk,
nsk: attacker_keys.nsk, nsk: attacker_keys.nsk,
membership_proof, membership_proof,
@ -807,7 +849,6 @@ mod tests {
let message = Message::try_from_circuit_output( let message = Message::try_from_circuit_output(
vec![victim_id, recipient_id], vec![victim_id, recipient_id],
vec![], // no public signers, no nonces vec![], // no public signers, no nonces
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
circuit_output, circuit_output,
) )
.unwrap(); .unwrap();

View File

@ -53,3 +53,33 @@ impl From<BasicAuth> for BasicAuthCredentials {
Self::new(value.username, value.password) 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);
}
}

View File

@ -93,4 +93,16 @@ mod tests {
let deserialized = HashType::from_str(&serialized).unwrap(); let deserialized = HashType::from_str(&serialized).unwrap();
assert_eq!(original, deserialized); 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);
}
} }

View File

@ -78,18 +78,9 @@ impl LeeTransaction {
block_id: BlockId, block_id: BlockId,
timestamp: Timestamp, timestamp: Timestamp,
) -> Result<ValidatedStateDiff, lee::error::LeeError> { ) -> Result<ValidatedStateDiff, lee::error::LeeError> {
let diff = match self { let diff = self.compute_state_diff(state, block_id, timestamp)?;
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)
}
}?;
// system accounts guard
let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([ let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([
lee::system_faucet_account_id(), lee::system_faucet_account_id(),
lee::system_bridge_account_id(), lee::system_bridge_account_id(),
@ -101,6 +92,28 @@ impl LeeTransaction {
Ok(diff) 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<ValidatedStateDiff, lee::error::LeeError> {
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 /// Validates the transaction against the current state, rejects modifications to clock
/// system accounts, and applies the resulting diff to the state. /// system accounts, and applies the resulting diff to the state.
pub fn execute_check_on_state( pub fn execute_check_on_state(
@ -115,6 +128,28 @@ impl LeeTransaction {
state.apply_state_diff(diff); state.apply_state_diff(diff);
Ok(self) 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<Self, lee::error::LeeError> {
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<lee::PublicTransaction> for LeeTransaction { impl From<lee::PublicTransaction> for LeeTransaction {
@ -188,3 +223,47 @@ fn validate_doesnt_modify_account(
Ok(()) 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",
);
}
}

View File

@ -171,7 +171,10 @@ impl IndexerStore {
transaction transaction
.clone() .clone()
.transaction_stateless_check()? .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, &mut state_guard,
block.header.block_id, block.header.block_id,
block.header.timestamp, block.header.timestamp,

View File

@ -7,8 +7,9 @@ use zeroize::Zeroizing;
pub mod python_path; pub mod python_path;
/// NSK and VSK as fixed-length zeroizing byte arrays. /// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>); /// arrays.
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 64]>);
// TODO: encrypt at rest alongside broader wallet storage encryption work. // TODO: encrypt at rest alongside broader wallet storage encryption work.
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -123,7 +124,7 @@ impl KeycardWallet {
} }
pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult<PublicKey> { pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult<PublicKey> {
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py)?; python_path::add_python_path(py)?;
let wallet = Self::new(py)?; let wallet = Self::new(py)?;
wallet.connect(py, pin)?; wallet.connect(py, pin)?;
@ -190,7 +191,7 @@ impl KeycardWallet {
path: &str, path: &str,
message: &[u8; 32], message: &[u8; 32],
) -> PyResult<(Signature, PublicKey)> { ) -> PyResult<(Signature, PublicKey)> {
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py)?; python_path::add_python_path(py)?;
let wallet = Self::new(py)?; let wallet = Self::new(py)?;
wallet.connect(py, pin)?; wallet.connect(py, pin)?;
@ -239,13 +240,13 @@ impl KeycardWallet {
}; };
let vsk = { let vsk = {
if raw_vsk.len() != 32 { if raw_vsk.len() != 64 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!( return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"expected 32-byte VSK from keycard, got {} bytes", "expected 64-byte VSK from keycard, got {} bytes",
raw_vsk.len() 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.copy_from_slice(&raw_vsk);
arr arr
}; };
@ -257,7 +258,7 @@ impl KeycardWallet {
pin: &str, pin: &str,
path: &str, path: &str,
) -> PyResult<PrivateKeyPair> { ) -> PyResult<PrivateKeyPair> {
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py)?; python_path::add_python_path(py)?;
let wallet = Self::new(py)?; let wallet = Self::new(py)?;
wallet.connect(py, pin)?; wallet.connect(py, pin)?;

View File

@ -48,7 +48,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> {
let sys = PyModule::import(py, "sys")?; let sys = PyModule::import(py, "sys")?;
let binding = sys.getattr("path")?; let binding = sys.getattr("path")?;
let sys_path = binding.downcast::<PyList>()?; let sys_path = binding.cast::<PyList>()?;
for path in &paths_to_add { for path in &paths_to_add {
let path_str = path.to_str().expect("Invalid path"); let path_str = path.to_str().expect("Invalid path");

View File

@ -208,7 +208,10 @@ impl RocksDBIO {
"transaction pre check failed with err {err:?}" "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, &mut breakpoint,
block.header.block_id, block.header.block_id,
block.header.timestamp, block.header.timestamp,

View File

@ -8,7 +8,7 @@ use lee_core::{
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey, Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
SharedSecretKey, SharedSecretKey,
account::{AccountWithMetadata, Nonce}, account::{AccountWithMetadata, Nonce},
encryption::{EphemeralPublicKey, ViewingPublicKey}, encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey},
}; };
use crate::{ExecutionFailureKind, WalletCore}; use crate::{ExecutionFailureKind, WalletCore};
@ -169,10 +169,7 @@ impl AccountIdentity {
} }
pub struct PrivateAccountKeys { pub struct PrivateAccountKeys {
pub npk: NullifierPublicKey,
pub ssk: SharedSecretKey, pub ssk: SharedSecretKey,
pub vpk: ViewingPublicKey,
pub epk: EphemeralPublicKey,
} }
enum State { enum State {
@ -376,12 +373,7 @@ impl AccountManager {
self.states self.states
.iter() .iter()
.filter_map(|state| match state { .filter_map(|state| match state {
State::Private(pre) => Some(PrivateAccountKeys { State::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }),
npk: pre.npk,
ssk: pre.ssk,
vpk: pre.vpk.clone(),
epk: pre.epk.clone(),
}),
State::Public { .. } | State::PublicKeycard { .. } => None, State::Public { .. } | State::PublicKeycard { .. } => None,
}) })
.collect() .collect()
@ -398,6 +390,8 @@ impl AccountManager {
State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public, State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public,
State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) { State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate { (Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk, ssk: pre.ssk,
nsk, nsk,
membership_proof, membership_proof,
@ -405,6 +399,8 @@ impl AccountManager {
seed: None, seed: None,
}, },
_ => InputAccountIdentity::PrivatePdaInit { _ => InputAccountIdentity::PrivatePdaInit {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
npk: pre.npk, npk: pre.npk,
ssk: pre.ssk, ssk: pre.ssk,
identifier: pre.identifier, identifier: pre.identifier,
@ -414,6 +410,8 @@ impl AccountManager {
State::Private(pre) => match (pre.nsk, pre.proof.clone()) { State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
(Some(nsk), Some(membership_proof)) => { (Some(nsk), Some(membership_proof)) => {
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk, ssk: pre.ssk,
nsk, nsk,
membership_proof, membership_proof,
@ -421,11 +419,15 @@ impl AccountManager {
} }
} }
(Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit { (Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
ssk: pre.ssk, ssk: pre.ssk,
nsk, nsk,
identifier: pre.identifier, identifier: pre.identifier,
}, },
(None, _) => InputAccountIdentity::PrivateUnauthorized { (None, _) => InputAccountIdentity::PrivateUnauthorized {
epk: pre.epk.clone(),
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
npk: pre.npk, npk: pre.npk,
ssk: pre.ssk, ssk: pre.ssk,
identifier: pre.identifier, identifier: pre.identifier,
@ -479,7 +481,7 @@ impl AccountManager {
.collect(); .collect();
if let Some(pin) = self.pin.clone() { 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)?; python_path::add_python_path(py)?;
let wallet = KeycardWallet::new(py)?; let wallet = KeycardWallet::new(py)?;
wallet.connect(py, &pin)?; wallet.connect(py, &pin)?;

View File

@ -39,7 +39,7 @@ impl WalletSubcommand for KeycardSubcommand {
) -> Result<SubcommandReturnValue> { ) -> Result<SubcommandReturnValue> {
match self { match self {
Self::Available => { Self::Available => {
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py) python_path::add_python_path(py)
.expect("`wallet::keycard::available`: unable to setup python path"); .expect("`wallet::keycard::available`: unable to setup python path");
@ -61,7 +61,7 @@ impl WalletSubcommand for KeycardSubcommand {
Self::Connect => { Self::Connect => {
let pin = read_pin()?; let pin = read_pin()?;
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py) python_path::add_python_path(py)
.expect("`wallet::keycard::connect`: unable to setup python path"); .expect("`wallet::keycard::connect`: unable to setup python path");
@ -81,7 +81,7 @@ impl WalletSubcommand for KeycardSubcommand {
Self::Disconnect => { Self::Disconnect => {
let pin = read_pin()?; let pin = read_pin()?;
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py) python_path::add_python_path(py)
.expect("`wallet::keycard::disconnect`: unable to setup python path"); .expect("`wallet::keycard::disconnect`: unable to setup python path");
@ -105,7 +105,7 @@ impl WalletSubcommand for KeycardSubcommand {
Self::Init => { Self::Init => {
let pin = read_pin()?; let pin = read_pin()?;
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py) python_path::add_python_path(py)
.expect("`wallet::keycard::init`: unable to setup python path"); .expect("`wallet::keycard::init`: unable to setup python path");
@ -128,7 +128,7 @@ impl WalletSubcommand for KeycardSubcommand {
let pin = read_pin()?; let pin = read_pin()?;
let mnemonic = read_mnemonic()?; let mnemonic = read_mnemonic()?;
Python::with_gil(|py| { Python::attach(|py| {
python_path::add_python_path(py) python_path::add_python_path(py)
.expect("`wallet::keycard::load`: unable to setup python path"); .expect("`wallet::keycard::load`: unable to setup python path");

View File

@ -587,10 +587,6 @@ impl WalletCore {
lee::privacy_preserving_transaction::message::Message::try_from_circuit_output( lee::privacy_preserving_transaction::message::Message::try_from_circuit_output(
acc_manager.public_account_ids(), acc_manager.public_account_ids(),
acc_manager.public_account_nonces(), acc_manager.public_account_nonces(),
private_account_keys
.iter()
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
.collect(),
output, output,
)?; )?;

View File

@ -1,7 +1,7 @@
use lee_core::{ use lee_core::{
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, InputAccountIdentity, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey, EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey, NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
account::{Account, AccountId, Nonce}, account::{Account, AccountId, Nonce},
compute_digest_for_path, compute_digest_for_path,
}; };
@ -17,7 +17,7 @@ pub fn compute_circuit_output(
let mut output = PrivacyPreservingCircuitOutput { let mut output = PrivacyPreservingCircuitOutput {
public_pre_states: Vec::new(), public_pre_states: Vec::new(),
public_post_states: Vec::new(), public_post_states: Vec::new(),
ciphertexts: Vec::new(), encrypted_private_post_states: Vec::new(),
new_commitments: Vec::new(), new_commitments: Vec::new(),
new_nullifiers: Vec::new(), new_nullifiers: Vec::new(),
block_validity_window, block_validity_window,
@ -40,6 +40,8 @@ pub fn compute_circuit_output(
output.public_post_states.push(post_state); output.public_post_states.push(post_state);
} }
InputAccountIdentity::PrivateAuthorizedInit { InputAccountIdentity::PrivateAuthorizedInit {
epk,
view_tag,
ssk, ssk,
nsk, nsk,
identifier, identifier,
@ -71,11 +73,15 @@ pub fn compute_circuit_output(
&account_id, &account_id,
&PrivateAccountKind::Regular(*identifier), &PrivateAccountKind::Regular(*identifier),
ssk, ssk,
epk,
*view_tag,
new_nullifier, new_nullifier,
new_nonce, new_nonce,
); );
} }
InputAccountIdentity::PrivateAuthorizedUpdate { InputAccountIdentity::PrivateAuthorizedUpdate {
epk,
view_tag,
ssk, ssk,
nsk, nsk,
membership_proof, membership_proof,
@ -105,11 +111,15 @@ pub fn compute_circuit_output(
&account_id, &account_id,
&PrivateAccountKind::Regular(*identifier), &PrivateAccountKind::Regular(*identifier),
ssk, ssk,
epk,
*view_tag,
new_nullifier, new_nullifier,
new_nonce, new_nonce,
); );
} }
InputAccountIdentity::PrivateUnauthorized { InputAccountIdentity::PrivateUnauthorized {
epk,
view_tag,
npk, npk,
ssk, ssk,
identifier, identifier,
@ -140,11 +150,15 @@ pub fn compute_circuit_output(
&account_id, &account_id,
&PrivateAccountKind::Regular(*identifier), &PrivateAccountKind::Regular(*identifier),
ssk, ssk,
epk,
*view_tag,
new_nullifier, new_nullifier,
new_nonce, new_nonce,
); );
} }
InputAccountIdentity::PrivatePdaInit { InputAccountIdentity::PrivatePdaInit {
epk,
view_tag,
npk: _, npk: _,
ssk, ssk,
identifier, identifier,
@ -187,11 +201,15 @@ pub fn compute_circuit_output(
identifier: *identifier, identifier: *identifier,
}, },
ssk, ssk,
epk,
*view_tag,
new_nullifier, new_nullifier,
new_nonce, new_nonce,
); );
} }
InputAccountIdentity::PrivatePdaUpdate { InputAccountIdentity::PrivatePdaUpdate {
epk,
view_tag,
ssk, ssk,
nsk, nsk,
membership_proof, membership_proof,
@ -231,6 +249,8 @@ pub fn compute_circuit_output(
identifier: *identifier, identifier: *identifier,
}, },
ssk, ssk,
epk,
*view_tag,
new_nullifier, new_nullifier,
new_nonce, new_nonce,
); );
@ -243,7 +263,7 @@ pub fn compute_circuit_output(
#[expect( #[expect(
clippy::too_many_arguments, 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( fn emit_private_output(
output: &mut PrivacyPreservingCircuitOutput, output: &mut PrivacyPreservingCircuitOutput,
@ -252,6 +272,8 @@ fn emit_private_output(
account_id: &AccountId, account_id: &AccountId,
kind: &PrivateAccountKind, kind: &PrivateAccountKind,
shared_secret: &SharedSecretKey, shared_secret: &SharedSecretKey,
epk: &EphemeralPublicKey,
view_tag: u8,
new_nullifier: (Nullifier, CommitmentSetDigest), new_nullifier: (Nullifier, CommitmentSetDigest),
new_nonce: Nonce, new_nonce: Nonce,
) { ) {
@ -270,7 +292,13 @@ fn emit_private_output(
); );
output.new_commitments.push(commitment_post); 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 *output_index = output_index
.checked_add(1) .checked_add(1)
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow")); .unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));

View File

@ -9,11 +9,14 @@
#![expect( #![expect(
clippy::arithmetic_side_effects, clippy::arithmetic_side_effects,
clippy::as_conversions,
clippy::cast_precision_loss,
clippy::float_arithmetic, clippy::float_arithmetic,
clippy::missing_const_for_fn, clippy::missing_const_for_fn,
clippy::non_ascii_literal, clippy::non_ascii_literal,
clippy::print_stderr, clippy::print_stderr,
clippy::print_stdout, clippy::print_stdout,
clippy::suboptimal_flops,
reason = "Bench tool: matches test-style fixture code" reason = "Bench tool: matches test-style fixture code"
)] )]
@ -68,6 +71,13 @@ struct BenchResult {
user_cycles: u64, user_cycles: u64,
segments: usize, segments: usize,
exec_stats: Stats, 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<f64>,
/// 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<f64>,
/// Stats over prover.prove(env, elf) wall-clock samples. Only populated when --prove is set. /// 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. /// Single-sample (n=1) when --prove is on without explicit repetition, since proving is slow.
prove_stats: Option<Stats>, prove_stats: Option<Stats>,
@ -81,6 +91,89 @@ struct BenchResult {
prove_segments: Option<usize>, prove_segments: Option<usize>,
} }
/// 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<Self> {
let n = results.len();
if n < 2 {
return None;
}
let xs: Vec<f64> = results.iter().map(|r| r.user_cycles as f64).collect();
let ys: Vec<f64> = 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 { struct Case {
program: &'static str, program: &'static str,
instruction_label: &'static str, instruction_label: &'static str,
@ -185,6 +278,8 @@ impl Case {
user_cycles: info.cycles(), user_cycles: info.cycles(),
segments: info.segments.len(), segments: info.segments.len(),
exec_stats, exec_stats,
net_compute_ms: None,
calibrated_ms: None,
prove_stats, prove_stats,
prove_total_cycles, prove_total_cycles,
prove_user_cycles, prove_user_cycles,
@ -495,12 +590,23 @@ fn main() -> Result<()> {
)?, )?,
]; ];
let results: Vec<BenchResult> = cases let mut results: Vec<BenchResult> = cases
.into_iter() .into_iter()
.map(|c| c.run(prove, exec_iters)) .map(|c| c.run(prove, exec_iters))
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
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); print_table(&results, prove);
if let Some(cal) = calibration {
print_calibration(&cal);
}
#[cfg(feature = "ppe")] #[cfg(feature = "ppe")]
let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() }; let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() };
@ -525,6 +631,7 @@ fn main() -> Result<()> {
} }
let combined = serde_json::json!({ let combined = serde_json::json!({
"standalone": results, "standalone": results,
"calibration": calibration,
"ppe": ppe_results, "ppe": ppe_results,
}); });
std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?; std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?;
@ -533,6 +640,24 @@ fn main() -> Result<()> {
Ok(()) 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) { fn print_table(results: &[BenchResult], prove: bool) {
let pw = results let pw = results
.iter() .iter()
@ -555,15 +680,28 @@ fn print_table(results: &[BenchResult], prove: bool) {
.unwrap_or(0) .unwrap_or(0)
.max("exec_ms (best / mean ± stdev)".len()); .max("exec_ms (best / mean ± stdev)".len());
let dw = 10_usize;
println!( println!(
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$}", "{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$} {:>dw$} {:>dw$}",
"program", "instruction", "user_cycles", "segments", "exec_ms (best / mean ± stdev)", "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 { 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!( println!(
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$}", "{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$} {:>dw$} {:>dw$}",
r.program, r.instruction, r.user_cycles, r.segments, r.exec_stats, 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());
}
}