mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-26 00:49:27 +00:00
Merge branch 'main' into Pravdyvy/programs-elfs-deployments
This commit is contained in:
commit
93feaba5de
60
Cargo.lock
generated
60
Cargo.lock
generated
@ -2038,7 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2542,7 +2542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3917,15 +3917,6 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
@ -6198,15 +6189,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mempool"
|
||||
version = "0.1.0"
|
||||
@ -7524,37 +7506,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.24.2"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
|
||||
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"once_cell",
|
||||
"portable-atomic",
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.24.2"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
|
||||
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.24.2"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
|
||||
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@ -7562,9 +7539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.24.2"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
|
||||
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@ -7574,13 +7551,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.24.2"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
|
||||
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
@ -8583,7 +8559,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -9630,7 +9606,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -10520,12 +10496,6 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
|
||||
[[package]]
|
||||
name = "unit-prefix"
|
||||
version = "0.5.2"
|
||||
@ -11056,7 +11026,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@ -165,7 +165,7 @@ actix-web = { version = "4.13.0", default-features = false, features = [
|
||||
] }
|
||||
clap = { version = "4.5.42", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
|
||||
pyo3 = { version = "0.24", features = ["auto-initialize"] }
|
||||
pyo3 = { version = "0.29", features = ["auto-initialize"] }
|
||||
zeroize = "1"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
|
||||
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.
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.
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.
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.
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.
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.
Binary file not shown.
@ -14,21 +14,37 @@ Per-program Risc0 cycle counts, prover wall time, PPE composition cost, and veri
|
||||
| Profile | release |
|
||||
| GPU acceleration | none |
|
||||
|
||||
## Executor cycles
|
||||
## Executor cycles and public-execution ms
|
||||
|
||||
`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over 5 timed iterations (1 warmup discarded).
|
||||
`SessionInfo::cycles()` per instruction. Deterministic across runs. Wall time is `best / mean ± stdev` over the timed iterations (1 warmup discarded; `--exec-iters` sets the count, 50 below). `calib_ms` and `net_ms` are the public-execution time in milliseconds, on the same axis as the private `G_verify` so the fee model has one common unit for both paths. See the calibration block below for how they are derived.
|
||||
|
||||
| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) |
|
||||
|---|---|---:|---:|---|
|
||||
| authenticated_transfer | Initialize | 43,642 | 1 | 18.86 / 19.41 ± 0.48 |
|
||||
| authenticated_transfer | Transfer | 77,095 | 1 | 19.67 / 20.84 ± 1.16 |
|
||||
| token | Burn | 116,546 | 1 | 24.86 / 25.46 ± 0.63 |
|
||||
| token | Mint | 116,862 | 1 | 24.47 / 25.08 ± 0.42 |
|
||||
| token | Transfer | 127,726 | 1 | 25.00 / 25.40 ± 0.29 |
|
||||
| clock | Tick (no rollups) | 137,022 | 1 | 21.18 / 21.57 ± 0.41 |
|
||||
| ata | Create | 175,056 | 1 | 23.64 / 24.94 ± 1.09 |
|
||||
| amm | SwapExactInput | 508,634 | 1 | 34.21 / 34.77 ± 0.55 |
|
||||
| amm | AddLiquidity | 642,774 | 1 | 37.59 / 37.87 ± 0.28 |
|
||||
| Program | Instruction | user_cycles | segments | exec_ms (best / mean ± stdev) | calib_ms | net_ms |
|
||||
|---|---|---:|---:|---|---:|---:|
|
||||
| authenticated_transfer | Initialize | 43,818 | 1 | 30.69 / 31.93 ± 1.03 | 1.31 | 0.29 |
|
||||
| authenticated_transfer | Transfer | 79,958 | 1 | 31.02 / 32.35 ± 0.59 | 2.38 | 0.61 |
|
||||
| token | Burn | 116,546 | 1 | 36.08 / 37.18 ± 0.60 | 3.47 | 5.67 |
|
||||
| token | Mint | 116,862 | 1 | 35.67 / 37.73 ± 2.54 | 3.48 | 5.26 |
|
||||
| token | Transfer | 127,726 | 1 | 35.49 / 36.86 ± 0.90 | 3.81 | 5.08 |
|
||||
| clock | Tick (no rollups) | 137,022 | 1 | 32.12 / 33.16 ± 0.89 | 4.08 | 1.72 |
|
||||
| ata | Create | 174,515 | 1 | 35.41 / 36.49 ± 0.65 | 5.20 | 5.00 |
|
||||
| amm | SwapExactInput | 508,904 | 1 | 46.71 / 48.06 ± 0.86 | 15.17 | 16.30 |
|
||||
| amm | AddLiquidity | 643,464 | 1 | 48.57 / 50.28 ± 0.98 | 19.18 | 18.16 |
|
||||
|
||||
### Public-execution ms calibration
|
||||
|
||||
The binary fits `best_ms = intercept + slope · user_cycles` by ordinary least squares across the nine cases (best-of-N, not mean, so one OS scheduling spike cannot tilt the slope). On the machine above:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| throughput (1 / slope) | 33,546 cycles/ms |
|
||||
| fixed overhead (intercept) | 30.41 ms per call |
|
||||
| R² | 0.935 |
|
||||
|
||||
- `calib_ms = user_cycles / throughput` is the compute-only time, a pure function of the deterministic cycle count and the one pinned-hardware constant, so it reproduces run to run where raw wall-time does not. This is the number to put on the common public/private ms axis.
|
||||
- `net_ms = best exec_ms − fixed overhead` is the measured compute with the host-side overhead stripped; it agrees with `calib_ms` to within the per-program overhead scatter (the intercept is an ELF-size-averaged constant, so this decomposition is first-order, not mechanistic).
|
||||
- The `fixed overhead` is host-side per-call setup (ELF parse into a `MemoryImage`, `ExecutorEnv` build) that is outside the cycle count and does not scale with the instruction's work.
|
||||
|
||||
The fixed overhead is paid per transaction in the current node, not amortized. The public-execution path at `lee/state_machine/src/program.rs:56-87` builds a fresh `ExecutorEnv` and calls `default_executor().execute(env, self.elf())` per call with the raw ELF bytes; no parsed image is cached across transactions. So today the real per-public-tx sequencer cost is the raw `exec_ms` (≈ 31 ms for the cheapest program), overhead-dominated. Caching the parsed `MemoryImage` per `ProgramId` would drop the per-tx cost to `calib_ms` (1–19 ms). Public execution is also cycle-capped at `MAX_NUM_CYCLES_PUBLIC_EXECUTION` (`program.rs:64`), which bounds the worst-case public-tx cost.
|
||||
|
||||
## Real proving (`--prove`)
|
||||
|
||||
@ -85,7 +101,8 @@ The corresponding `proof_bytes` (S_agg) for the bench receipt is captured by `--
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
cargo run --release -p cycle_bench
|
||||
# Executor cycles + public-execution ms calibration (no proving). --exec-iters sets the sample count.
|
||||
cargo run --release -p cycle_bench -- --exec-iters 50
|
||||
cargo run --release -p cycle_bench --features prove -- --prove
|
||||
cargo run --release -p cycle_bench --features ppe -- --prove --ppe
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ use lee::{
|
||||
privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::AccountWithMetadata,
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
};
|
||||
@ -665,9 +665,9 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
let auth_transfer_program_id = Program::authenticated_transfer_program().id();
|
||||
let nsk: lee_core::NullifierSecretKey = [3; 32];
|
||||
let npk = NullifierPublicKey::from(&nsk);
|
||||
let _vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let vpk = ViewingPublicKey::from_bytes(vec![4_u8; 1184]).unwrap();
|
||||
let ssk = SharedSecretKey([55_u8; 32]);
|
||||
let _epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
let epk = EphemeralPublicKey(vec![55_u8; 1088]);
|
||||
let attacker_vault_id = {
|
||||
let seed = vault_core::compute_vault_seed(attacker_id);
|
||||
AccountId::for_private_pda(&vault_program_id, &seed, &npk, 1337)
|
||||
@ -712,6 +712,8 @@ async fn ppt_cant_chain_call_faucet() -> Result<()> {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
npk,
|
||||
ssk,
|
||||
identifier: 1337,
|
||||
|
||||
@ -4,12 +4,14 @@
|
||||
reason = "We don't care about these in tests"
|
||||
)]
|
||||
|
||||
use std::time::Duration;
|
||||
use std::{ops::Deref as _, time::Duration};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use borsh::BorshSerialize;
|
||||
use common::transaction::LeeTransaction;
|
||||
use integration_tests::{TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext};
|
||||
use integration_tests::{
|
||||
TIME_TO_WAIT_FOR_BLOCK_SECONDS, TestContext, wait_for_indexer_to_catch_up,
|
||||
};
|
||||
use lee::{
|
||||
AccountId, execute_and_prove, privacy_preserving_transaction, program::Program,
|
||||
public_transaction,
|
||||
@ -150,7 +152,6 @@ async fn private_bridge_deposit_invocation_is_dropped() -> anyhow::Result<()> {
|
||||
let message = privacy_preserving_transaction::Message::try_from_circuit_output(
|
||||
vec![bridge_account_id, recipient_vault_id],
|
||||
vec![bridge_pre.account.nonce, vault_pre.account.nonce],
|
||||
vec![],
|
||||
output,
|
||||
)
|
||||
.context("Failed to build privacy-preserving bridge deposit message")?;
|
||||
@ -450,5 +451,26 @@ async fn bedrock_deposit_mints_to_vault_then_claim_succeeds() -> anyhow::Result<
|
||||
"Recipient balance should increase by claimed amount"
|
||||
);
|
||||
|
||||
// The indexer must replay the deposit and claim blocks and reach the same
|
||||
// state as the sequencer — including the bridge system account the deposit
|
||||
// modifies, which is the case the hot fix unblocks.
|
||||
wait_for_indexer_to_catch_up(&ctx).await?;
|
||||
let bridge_account_id = lee::system_bridge_account_id();
|
||||
for account_id in [recipient_id, recipient_vault_id, bridge_account_id] {
|
||||
let indexer_account = indexer_service_rpc::RpcClient::get_account(
|
||||
// `deref` is needed for correct trait resolution
|
||||
// of the async `get_account` method on `RpcClient`
|
||||
ctx.indexer_client().deref(),
|
||||
account_id.into(),
|
||||
)
|
||||
.await?;
|
||||
let sequencer_account = ctx.sequencer_client().get_account(account_id).await?;
|
||||
assert_eq!(
|
||||
indexer_account,
|
||||
sequencer_account.into(),
|
||||
"Indexer and sequencer diverged for account {account_id} after deposit"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ use lee::{
|
||||
program::Program,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, NullifierPublicKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::ViewingPublicKey,
|
||||
program::PdaSeed,
|
||||
@ -74,6 +74,8 @@ async fn fund_private_pda(
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &vpk),
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
@ -89,13 +91,9 @@ async fn fund_private_pda(
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("circuit proving failed: {e}"))?;
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender],
|
||||
vec![sender_account.nonce],
|
||||
vec![(npk, vpk, epk)],
|
||||
output,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![sender], vec![sender_account.nonce], output)
|
||||
.map_err(|e| anyhow::anyhow!("message build failed: {e}"))?;
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[sender_sk]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
@ -23,7 +23,7 @@ use lee::{
|
||||
public_transaction as putx,
|
||||
};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
EncryptedAccountData, InputAccountIdentity, MembershipProof, NullifierPublicKey,
|
||||
account::{AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::ViewingPublicKey,
|
||||
};
|
||||
@ -301,12 +301,16 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: sender_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&sender_npk, &sender_vpk),
|
||||
ssk: sender_ss,
|
||||
nsk: sender_nsk,
|
||||
membership_proof: proof,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: recipient_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&recipient_npk, &recipient_vpk),
|
||||
npk: recipient_npk,
|
||||
ssk: recipient_ss,
|
||||
identifier: 0,
|
||||
@ -315,16 +319,7 @@ fn build_privacy_transaction() -> PrivacyPreservingTransaction {
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(sender_npk, sender_vpk, sender_epk),
|
||||
(recipient_npk, recipient_vpk, recipient_epk),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = pptx::message::Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let witness_set = pptx::witness_set::WitnessSet::for_message(&message, proof, &[]);
|
||||
pptx::PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
|
||||
use k256::elliptic_curve::PrimeField as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::key_management::key_tree::traits::KeyTreeNode;
|
||||
@ -6,9 +6,13 @@ use crate::key_management::key_tree::traits::KeyTreeNode;
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))]
|
||||
pub struct ChildKeysPublic {
|
||||
pub csk: lee::PrivateKey,
|
||||
pub cpk: lee::PublicKey,
|
||||
pub ccc: [u8; 32],
|
||||
/// Secret key for public account.
|
||||
pub sk: lee::PrivateKey,
|
||||
/// Schnorr secret key.
|
||||
pub ssk: lee::PrivateKey,
|
||||
/// Schnorr public key.
|
||||
pub pk: lee::PublicKey,
|
||||
pub cc: [u8; 32],
|
||||
/// Can be [`None`] if root.
|
||||
pub cci: Option<u32>,
|
||||
}
|
||||
@ -18,19 +22,24 @@ impl ChildKeysPublic {
|
||||
pub fn root(seed: [u8; 64]) -> Self {
|
||||
let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub");
|
||||
|
||||
let csk = lee::PrivateKey::try_new(
|
||||
let sk = lee::PrivateKey::try_new(
|
||||
*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"),
|
||||
)
|
||||
.expect("Expect a valid Private Key");
|
||||
let ccc = *hash_value.last_chunk::<32>().unwrap();
|
||||
let cpk = lee::PublicKey::new_from_private_key(&csk);
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`");
|
||||
|
||||
let cc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
sk,
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: None,
|
||||
}
|
||||
}
|
||||
@ -39,61 +48,53 @@ impl ChildKeysPublic {
|
||||
pub fn nth_child(&self, cci: u32) -> Self {
|
||||
let hash_value = self.compute_hash_value(cci);
|
||||
|
||||
let csk = lee::PrivateKey::try_new({
|
||||
let hash_value = hash_value
|
||||
let lhs = k256::Scalar::from_repr(
|
||||
(*hash_value
|
||||
.first_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32");
|
||||
.expect("hash_value is 64 bytes, must be safe to get first 32"))
|
||||
.into(),
|
||||
)
|
||||
.expect("Expect a valid k256 scalar");
|
||||
let rhs =
|
||||
k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar");
|
||||
|
||||
let value_1 =
|
||||
k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar");
|
||||
let value_2 = k256::Scalar::from_repr((*self.csk.value()).into())
|
||||
.expect("Expect a valid k256 scalar");
|
||||
let sk = lee::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into())
|
||||
.expect("Expect a valid private key");
|
||||
|
||||
let sum = value_1.add(&value_2);
|
||||
sum.to_bytes().into()
|
||||
})
|
||||
.expect("Expect a valid private key");
|
||||
let ssk = lee::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`");
|
||||
|
||||
let ccc = *hash_value
|
||||
let cc = *hash_value
|
||||
.last_chunk::<32>()
|
||||
.expect("hash_value is 64 bytes, must be safe to get last 32");
|
||||
|
||||
let cpk = lee::PublicKey::new_from_private_key(&csk);
|
||||
let pk = lee::PublicKey::new_from_private_key(&ssk);
|
||||
|
||||
Self {
|
||||
csk,
|
||||
cpk,
|
||||
ccc,
|
||||
sk,
|
||||
ssk,
|
||||
pk,
|
||||
cc,
|
||||
cci: Some(cci),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn account_id(&self) -> lee::AccountId {
|
||||
lee::AccountId::from(&self.cpk)
|
||||
lee::AccountId::from(&self.pk)
|
||||
}
|
||||
|
||||
fn compute_hash_value(&self, cci: u32) -> [u8; 64] {
|
||||
let mut hash_input = vec![];
|
||||
|
||||
if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater {
|
||||
// Non-harden.
|
||||
// BIP-032 compatibility requires 1-byte header from the public_key;
|
||||
// Not stored in `self.cpk.value()`.
|
||||
let sk = k256::SecretKey::from_bytes(self.csk.value().into())
|
||||
.expect("32 bytes, within curve order");
|
||||
let pk = sk.public_key();
|
||||
hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes());
|
||||
} else {
|
||||
// Harden.
|
||||
hash_input.extend_from_slice(&[0_u8]);
|
||||
hash_input.extend_from_slice(self.csk.value());
|
||||
}
|
||||
// Simplified key logic by only supporting harden keys.
|
||||
// Non-harden keys would require access to untweaked public keys associated to `sk`s.
|
||||
// Thus, not PQ secure.
|
||||
hash_input.extend_from_slice(&[0_u8]);
|
||||
hash_input.extend_from_slice(self.sk.value());
|
||||
|
||||
#[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")]
|
||||
hash_input.extend_from_slice(&cci.to_be_bytes());
|
||||
|
||||
hmac_sha512::HMAC::mac(hash_input, self.ccc)
|
||||
hmac_sha512::HMAC::mac(hash_input, self.cc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +104,7 @@ impl ChildKeysPublic {
|
||||
)]
|
||||
impl<'a> From<&'a ChildKeysPublic> for &'a lee::PrivateKey {
|
||||
fn from(value: &'a ChildKeysPublic) -> Self {
|
||||
&value.csk
|
||||
&value.ssk
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,30 +138,37 @@ mod tests {
|
||||
];
|
||||
let keys = ChildKeysPublic::root(seed);
|
||||
|
||||
let expected_ccc = [
|
||||
let expected_cc = [
|
||||
238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222,
|
||||
13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
let expected_sk: PrivateKey = PrivateKey::try_new([
|
||||
40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36,
|
||||
202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247,
|
||||
244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245,
|
||||
let expected_ssk: PrivateKey = PrivateKey::try_new([
|
||||
207, 4, 246, 223, 104, 72, 19, 85, 14, 122, 194, 82, 32, 163, 60, 57, 8, 25, 209, 91,
|
||||
254, 107, 76, 238, 31, 68, 236, 192, 154, 78, 105, 118,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == keys.ccc);
|
||||
assert!(expected_csk == keys.csk);
|
||||
assert!(expected_cpk == keys.cpk);
|
||||
let expected_pk: PublicKey = PublicKey::try_new([
|
||||
188, 163, 203, 45, 151, 154, 230, 254, 123, 114, 158, 130, 19, 182, 164, 143, 150, 131,
|
||||
176, 7, 27, 58, 204, 116, 5, 247, 0, 255, 111, 160, 52, 201,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_cc == keys.cc);
|
||||
assert!(expected_ssk == keys.ssk);
|
||||
assert!(expected_sk == keys.sk);
|
||||
assert!(expected_pk == keys.pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harden_child_keys_generation() {
|
||||
fn child_keys_generation() {
|
||||
let seed = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||
@ -171,93 +179,32 @@ mod tests {
|
||||
let cci = (2_u32).pow(31) + 13;
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
let expected_cc = [
|
||||
149, 226, 13, 4, 194, 12, 69, 29, 9, 234, 209, 119, 98, 4, 128, 91, 37, 103, 192, 31,
|
||||
130, 126, 123, 20, 90, 34, 173, 209, 101, 248, 155, 36,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
let expected_sk: PrivateKey = PrivateKey::try_new([
|
||||
9, 65, 33, 228, 25, 82, 219, 117, 91, 217, 11, 223, 144, 85, 246, 26, 123, 216, 107,
|
||||
213, 33, 52, 188, 22, 198, 246, 71, 46, 245, 174, 16, 47,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
142, 143, 238, 159, 105, 165, 224, 252, 108, 62, 53, 209, 176, 219, 249, 38, 90, 241,
|
||||
201, 81, 194, 146, 236, 5, 83, 152, 238, 243, 138, 16, 229, 15,
|
||||
let expected_ssk: PrivateKey = PrivateKey::try_new([
|
||||
100, 37, 212, 81, 40, 233, 72, 156, 177, 139, 50, 114, 136, 157, 202, 132, 203, 246,
|
||||
252, 242, 13, 81, 42, 100, 159, 240, 187, 252, 202, 108, 25, 105,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonharden_child_keys_generation() {
|
||||
let seed = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||
187, 148, 92, 44, 253, 210, 37,
|
||||
];
|
||||
let root_keys = ChildKeysPublic::root(seed);
|
||||
let cci = 13;
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
79, 228, 242, 119, 211, 203, 198, 175, 95, 36, 4, 234, 139, 45, 137, 138, 54, 211, 187,
|
||||
16, 28, 79, 80, 232, 216, 101, 145, 19, 101, 220, 217, 141,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
185, 147, 32, 242, 145, 91, 123, 77, 42, 33, 134, 84, 12, 165, 117, 70, 158, 201, 95,
|
||||
153, 14, 12, 92, 235, 128, 156, 194, 169, 68, 35, 165, 127,
|
||||
let expected_pk: PublicKey = PublicKey::try_new([
|
||||
210, 59, 119, 137, 21, 153, 82, 22, 195, 82, 12, 16, 80, 156, 125, 199, 19, 173, 46,
|
||||
224, 213, 144, 165, 126, 70, 129, 171, 141, 77, 212, 108, 233,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
119, 16, 145, 121, 97, 244, 186, 35, 136, 34, 140, 171, 206, 139, 11, 208, 207, 121,
|
||||
158, 45, 28, 22, 140, 98, 161, 179, 212, 173, 238, 220, 2, 34,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_case_child_keys_generation_2_power_31() {
|
||||
let seed = [
|
||||
88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173,
|
||||
134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87,
|
||||
22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6,
|
||||
187, 148, 92, 44, 253, 210, 37,
|
||||
];
|
||||
let root_keys = ChildKeysPublic::root(seed);
|
||||
let cci = (2_u32).pow(31); //equivant to 0, thus non-harden.
|
||||
let child_keys = ChildKeysPublic::nth_child(&root_keys, cci);
|
||||
|
||||
let expected_ccc = [
|
||||
221, 208, 47, 189, 174, 152, 33, 25, 151, 114, 233, 191, 57, 15, 40, 140, 46, 87, 126,
|
||||
58, 215, 40, 246, 111, 166, 113, 183, 145, 173, 11, 27, 182,
|
||||
];
|
||||
|
||||
let expected_csk: PrivateKey = PrivateKey::try_new([
|
||||
223, 29, 87, 189, 126, 24, 117, 225, 190, 57, 0, 143, 207, 168, 231, 139, 170, 192, 81,
|
||||
254, 126, 10, 115, 42, 141, 157, 70, 171, 199, 231, 198, 132,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let expected_cpk: PublicKey = PublicKey::try_new([
|
||||
96, 123, 245, 51, 214, 216, 215, 205, 70, 145, 105, 221, 166, 169, 122, 27, 94, 112,
|
||||
228, 110, 249, 177, 85, 173, 180, 248, 185, 199, 112, 246, 83, 33,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert!(expected_ccc == child_keys.ccc);
|
||||
assert!(expected_csk == child_keys.csk);
|
||||
assert!(expected_cpk == child_keys.cpk);
|
||||
assert!(expected_cc == child_keys.cc);
|
||||
assert!(expected_ssk == child_keys.ssk);
|
||||
assert!(expected_sk == child_keys.sk);
|
||||
assert!(expected_pk == child_keys.pk);
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,8 +347,8 @@ mod tests {
|
||||
|
||||
assert!(tree.key_map.contains_key(&ChainIndex::root()));
|
||||
assert!(tree.account_id_map.contains_key(&AccountId::new([
|
||||
172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178,
|
||||
38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247
|
||||
10, 231, 159, 65, 236, 46, 205, 5, 172, 89, 250, 29, 123, 195, 212, 137, 155, 111, 40,
|
||||
120, 53, 28, 124, 54, 224, 170, 119, 208, 2, 72, 75, 50
|
||||
])));
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ use crate::{
|
||||
Commitment, CommitmentSetDigest, Identifier, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
encryption::Ciphertext,
|
||||
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewTag},
|
||||
program::{BlockValidityWindow, PdaSeed, ProgramId, ProgramOutput, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
@ -33,6 +33,8 @@ pub enum InputAccountIdentity {
|
||||
/// `AccountId::for_regular_private_account(&NullifierPublicKey::from(nsk), identifier)` and
|
||||
/// matched against `pre_state.account_id`.
|
||||
PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -40,6 +42,8 @@ pub enum InputAccountIdentity {
|
||||
/// Update of an authorized standalone private account: existing on-chain commitment, with
|
||||
/// membership proof.
|
||||
PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
@ -48,6 +52,8 @@ pub enum InputAccountIdentity {
|
||||
/// Init of a standalone private account the caller does not own (e.g. a recipient who
|
||||
/// doesn't yet exist on chain). No `nsk`, no membership proof.
|
||||
PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -57,6 +63,8 @@ pub enum InputAccountIdentity {
|
||||
/// PDA within the `(program_id, seed, npk)` family: `AccountId::for_private_pda` uses it
|
||||
/// as the 4th input.
|
||||
PrivatePdaInit {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
npk: NullifierPublicKey,
|
||||
ssk: SharedSecretKey,
|
||||
identifier: Identifier,
|
||||
@ -72,6 +80,8 @@ pub enum InputAccountIdentity {
|
||||
/// from `nsk`. Authorization may be established upstream by a caller `pda_seeds` match or a
|
||||
/// previously-seen authorization in a chained call.
|
||||
PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey,
|
||||
view_tag: ViewTag,
|
||||
ssk: SharedSecretKey,
|
||||
nsk: NullifierSecretKey,
|
||||
membership_proof: MembershipProof,
|
||||
@ -123,7 +133,7 @@ impl InputAccountIdentity {
|
||||
pub struct PrivacyPreservingCircuitOutput {
|
||||
pub public_pre_states: Vec<AccountWithMetadata>,
|
||||
pub public_post_states: Vec<Account>,
|
||||
pub ciphertexts: Vec<Ciphertext>,
|
||||
pub encrypted_private_post_states: Vec<EncryptedAccountData>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<(Nullifier, CommitmentSetDigest)>,
|
||||
pub block_validity_window: BlockValidityWindow,
|
||||
@ -148,6 +158,7 @@ mod tests {
|
||||
use crate::{
|
||||
Commitment, Nullifier,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce},
|
||||
encryption::Ciphertext,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -181,7 +192,11 @@ mod tests {
|
||||
data: b"post state data".to_vec().try_into().unwrap(),
|
||||
nonce: Nonce(0xFFFF_FFFF_FFFF_FFFF),
|
||||
}],
|
||||
ciphertexts: vec![Ciphertext(vec![255, 255, 1, 1, 2, 2])],
|
||||
encrypted_private_post_states: vec![EncryptedAccountData {
|
||||
ciphertext: Ciphertext(vec![255, 255, 1, 1, 2, 2]),
|
||||
epk: EphemeralPublicKey(vec![9, 9, 9]),
|
||||
view_tag: 42,
|
||||
}],
|
||||
new_commitments: vec![Commitment::new(
|
||||
&AccountId::new([1; 32]),
|
||||
&Account::default(),
|
||||
|
||||
@ -6,7 +6,7 @@ use chacha20::{
|
||||
use risc0_zkvm::sha::{Impl, Sha256 as _};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "host")]
|
||||
pub use shared_key_derivation::{EphemeralPublicKey, MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
pub use shared_key_derivation::{MlKem768EncapsulationKey, ViewingPublicKey};
|
||||
|
||||
use crate::{Commitment, account::Account, program::PrivateAccountKind};
|
||||
#[cfg(feature = "host")]
|
||||
@ -17,6 +17,11 @@ pub type Scalar = [u8; 32];
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct SharedSecretKey(pub [u8; 32]);
|
||||
|
||||
/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the
|
||||
/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
|
||||
pub struct EncryptionScheme;
|
||||
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
@ -36,6 +41,45 @@ impl std::fmt::Debug for Ciphertext {
|
||||
}
|
||||
}
|
||||
|
||||
pub type ViewTag = u8;
|
||||
|
||||
/// Encrypted private-account note for one output.
|
||||
#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(Debug, Clone, PartialEq, Eq))]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
impl EncryptedAccountData {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
ciphertext: Ciphertext,
|
||||
npk: &crate::NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
epk: EphemeralPublicKey,
|
||||
) -> Self {
|
||||
let view_tag = Self::compute_view_tag(npk, vpk);
|
||||
Self {
|
||||
ciphertext,
|
||||
epk,
|
||||
view_tag,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || npk || vpk).
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &crate::NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(b"/LEE/v0.3/ViewTag/");
|
||||
bytes.extend_from_slice(&npk.to_byte_array());
|
||||
bytes.extend_from_slice(vpk.to_bytes());
|
||||
Impl::hash_bytes(&bytes).as_bytes()[0]
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptionScheme {
|
||||
#[must_use]
|
||||
pub fn encrypt(
|
||||
|
||||
@ -2,12 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use ml_kem::{Decapsulate as _, Encapsulate as _, KeyExport as _, Seed};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::SharedSecretKey;
|
||||
|
||||
/// The ML-KEM-768 ciphertext produced during encapsulation; transmitted on-wire in place of the
|
||||
/// former ECDH ephemeral public key. Always 1088 bytes for ML-KEM-768.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EphemeralPublicKey(pub Vec<u8>);
|
||||
use crate::{EphemeralPublicKey, SharedSecretKey};
|
||||
|
||||
/// ML-KEM-768 encapsulation key bytes (1184 bytes, opaque to this crate).
|
||||
#[derive(
|
||||
|
||||
@ -10,7 +10,9 @@ pub use commitment::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, DUMMY_COMMITMENT_HASH, MembershipProof,
|
||||
compute_digest_for_path,
|
||||
};
|
||||
pub use encryption::{EncryptionScheme, SharedSecretKey};
|
||||
pub use encryption::{
|
||||
EncryptedAccountData, EncryptionScheme, EphemeralPublicKey, SharedSecretKey, ViewTag,
|
||||
};
|
||||
pub use nullifier::{Identifier, Nullifier, NullifierPublicKey, NullifierSecretKey};
|
||||
pub use program::PrivateAccountKind;
|
||||
|
||||
|
||||
@ -178,8 +178,8 @@ mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptionScheme, Nullifier,
|
||||
PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
@ -201,7 +201,7 @@ mod tests {
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[idx],
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
@ -210,6 +210,17 @@ mod tests {
|
||||
kind
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_inner_roundtrip() {
|
||||
// `Proof::from_inner(b).into_inner()` must return exactly `b`. Catches
|
||||
// mutations of `into_inner` returning `vec![]`, `vec![0]`, or `vec![1]`,
|
||||
// and of `from_inner` discarding its argument.
|
||||
let bytes = vec![0xDE_u8, 0xAD, 0xBE, 0xEF];
|
||||
assert_eq!(Proof::from_inner(bytes.clone()).into_inner(), bytes);
|
||||
assert!(Proof::from_inner(vec![]).into_inner().is_empty());
|
||||
assert_eq!(Proof::from_inner(vec![0xFF]).into_inner(), vec![0xFF_u8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_public_and_private_pre_accounts() {
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
@ -257,6 +268,11 @@ mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -274,10 +290,10 @@ mod tests {
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[0],
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
@ -356,6 +372,11 @@ mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret_1,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: commitment_set
|
||||
@ -364,6 +385,11 @@ mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret_2,
|
||||
identifier: 0,
|
||||
@ -378,10 +404,10 @@ mod tests {
|
||||
assert!(output.public_post_states.is_empty());
|
||||
assert_eq!(output.new_commitments, expected_new_commitments);
|
||||
assert_eq!(output.new_nullifiers, expected_new_nullifiers);
|
||||
assert_eq!(output.ciphertexts.len(), 2);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[0],
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
@ -390,7 +416,7 @@ mod tests {
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.ciphertexts[1],
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
@ -432,6 +458,11 @@ mod tests {
|
||||
vec![pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -461,6 +492,8 @@ mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier,
|
||||
@ -508,6 +541,8 @@ mod tests {
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
@ -561,6 +596,8 @@ mod tests {
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
@ -618,6 +655,11 @@ mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&shared_npk,
|
||||
&shared_keys.vpk(),
|
||||
),
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
@ -647,6 +689,8 @@ mod tests {
|
||||
Program::serialize_instruction(authenticated_transfer_core::Instruction::Initialize)
|
||||
.unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
@ -691,6 +735,8 @@ mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
@ -735,6 +781,8 @@ mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&commitment).unwrap(),
|
||||
@ -789,6 +837,8 @@ mod tests {
|
||||
Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
@ -827,6 +877,8 @@ mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: 99,
|
||||
@ -870,6 +922,8 @@ mod tests {
|
||||
Program::serialize_instruction((seed, 1_u128, auth_transfer_id, false)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
membership_proof: commitment_set.get_proof_for(&pda_commitment).unwrap(),
|
||||
|
||||
@ -1,52 +1,16 @@
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, Nullifier, NullifierPublicKey, PrivacyPreservingCircuitOutput,
|
||||
Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput,
|
||||
account::{Account, Nonce},
|
||||
encryption::{Ciphertext, EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
pub use lee_core::{EncryptedAccountData, ViewTag};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::{AccountId, error::LeeError};
|
||||
|
||||
const PREFIX: &[u8; 32] = b"/LEE/v0.3/Message/Privacy/\x00\x00\x00\x00\x00\x00";
|
||||
|
||||
pub type ViewTag = u8;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct EncryptedAccountData {
|
||||
pub ciphertext: Ciphertext,
|
||||
pub epk: EphemeralPublicKey,
|
||||
pub view_tag: ViewTag,
|
||||
}
|
||||
|
||||
impl EncryptedAccountData {
|
||||
fn new(
|
||||
ciphertext: Ciphertext,
|
||||
npk: &NullifierPublicKey,
|
||||
vpk: &ViewingPublicKey,
|
||||
epk: EphemeralPublicKey,
|
||||
) -> Self {
|
||||
let view_tag = Self::compute_view_tag(npk, vpk);
|
||||
Self {
|
||||
ciphertext,
|
||||
epk,
|
||||
view_tag,
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the tag as the first byte of SHA256("/LEE/v0.3/ViewTag/" || Npk || vpk).
|
||||
#[must_use]
|
||||
pub fn compute_view_tag(npk: &NullifierPublicKey, vpk: &ViewingPublicKey) -> ViewTag {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(b"/LEE/v0.3/ViewTag/");
|
||||
hasher.update(npk.to_byte_array());
|
||||
hasher.update(vpk.to_bytes());
|
||||
let digest: [u8; 32] = hasher.finalize().into();
|
||||
digest[0]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Message {
|
||||
pub public_account_ids: Vec<AccountId>,
|
||||
@ -92,28 +56,13 @@ impl Message {
|
||||
pub fn try_from_circuit_output(
|
||||
public_account_ids: Vec<AccountId>,
|
||||
nonces: Vec<Nonce>,
|
||||
public_keys: Vec<(NullifierPublicKey, ViewingPublicKey, EphemeralPublicKey)>,
|
||||
output: PrivacyPreservingCircuitOutput,
|
||||
) -> 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 {
|
||||
public_account_ids,
|
||||
nonces,
|
||||
public_post_states: output.public_post_states,
|
||||
encrypted_private_post_states,
|
||||
encrypted_private_post_states: output.encrypted_private_post_states,
|
||||
new_commitments: output.new_commitments,
|
||||
new_nullifiers: output.new_nullifiers,
|
||||
block_validity_window: output.block_validity_window,
|
||||
|
||||
@ -498,6 +498,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elf_returns_the_program_bytecode_constant() {
|
||||
// `Program::elf` must return exactly the compile-time ELF, never an empty
|
||||
// or placeholder slice. Catches mutations returning `Vec::leak(Vec::new())`,
|
||||
// `Vec::leak(vec![0])`, or `Vec::leak(vec![1])`.
|
||||
let at = Program::authenticated_transfer_program();
|
||||
assert!(!at.elf().is_empty());
|
||||
assert_eq!(at.elf(), AUTHENTICATED_TRANSFER_ELF);
|
||||
|
||||
let token = Program::token();
|
||||
assert!(!token.elf().is_empty());
|
||||
assert_eq!(token.elf(), TOKEN_ELF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = Program::simple_balance_transfer();
|
||||
|
||||
@ -16,3 +16,18 @@ impl Message {
|
||||
self.bytecode
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Message;
|
||||
|
||||
#[test]
|
||||
fn bytecode_roundtrip() {
|
||||
// `Message::new(b).into_bytecode()` must return exactly `b`. Catches
|
||||
// mutations of `into_bytecode` returning `vec![]`, `vec![0]`, or `vec![1]`.
|
||||
let bytecode = vec![0x7F_u8, 0x45, 0x4C, 0x46]; // ELF magic
|
||||
assert_eq!(Message::new(bytecode.clone()).into_bytecode(), bytecode);
|
||||
assert!(Message::new(vec![]).into_bytecode().is_empty());
|
||||
assert_eq!(Message::new(vec![0xAB]).into_bytecode(), vec![0xAB_u8]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _};
|
||||
use rand::{Rng as _, rngs::OsRng};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
use crate::error::LeeError;
|
||||
|
||||
@ -60,6 +62,29 @@ impl PrivateKey {
|
||||
pub const fn value(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// `tweak` produces the "tweaked secret key" (`sk`) given a public account's `ssk`.
|
||||
/// We use "tweaked keys" to shield the public accounts' `ssk` against quantum threats.
|
||||
/// The "tweaked keys" are used for Schnorr Signatures (BIP-340).
|
||||
/// The usage of these keys will be greatly reduced once LEE is upgraded to use a PQ signatures.
|
||||
pub fn tweak(value: &[u8; 32]) -> Result<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)]
|
||||
@ -75,4 +100,33 @@ mod tests {
|
||||
fn produce_key() {
|
||||
let _key = PrivateKey::new_os_random();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tweak_rejects_zero_key() {
|
||||
assert!(matches!(
|
||||
PrivateKey::tweak(&[0_u8; 32]),
|
||||
Err(LeeError::InvalidPrivateKey)
|
||||
));
|
||||
}
|
||||
|
||||
// tweak: 0xFF…FF exceeds the secp256k1 curve order
|
||||
#[test]
|
||||
fn tweak_rejects_out_of_range_key() {
|
||||
assert!(matches!(
|
||||
PrivateKey::tweak(&[0xFF; 32]),
|
||||
Err(LeeError::InvalidPrivateKey)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tweak_deterministic() {
|
||||
let tweaked = PrivateKey::tweak(&[1_u8; 32]).unwrap();
|
||||
assert_eq!(
|
||||
tweaked.value(),
|
||||
&[
|
||||
242, 210, 33, 19, 65, 108, 136, 176, 179, 128, 110, 210, 107, 193, 168, 112, 206,
|
||||
171, 86, 238, 131, 10, 39, 36, 44, 39, 246, 20, 46, 193, 204, 66
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,8 +418,8 @@ pub mod tests {
|
||||
|
||||
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
|
||||
use lee_core::{
|
||||
BlockId, Commitment, InputAccountIdentity, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, SharedSecretKey, Timestamp,
|
||||
BlockId, Commitment, EncryptedAccountData, InputAccountIdentity, Nullifier,
|
||||
NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
program::{
|
||||
@ -613,6 +613,48 @@ pub mod tests {
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_system_accounts_have_expected_contents() {
|
||||
// System-account IDs must be distinct and non-default, and the genesis
|
||||
// faucet/bridge accounts must carry their expected field values. Catches
|
||||
// mutations that replace `system_faucet_account`/`system_bridge_account`
|
||||
// with `Default::default()`, delete their `balance`/`program_owner`
|
||||
// fields, or replace `system_bridge_account_id` with `Default::default()`.
|
||||
let faucet_id = system_faucet_account_id();
|
||||
let bridge_id = system_bridge_account_id();
|
||||
assert_ne!(bridge_id, AccountId::default());
|
||||
assert_ne!(faucet_id, bridge_id);
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
let default_owner = Account::default().program_owner;
|
||||
|
||||
let faucet = state.get_account_by_id(faucet_id);
|
||||
assert_eq!(faucet.balance, u128::MAX, "faucet must hold u128::MAX");
|
||||
assert_ne!(
|
||||
faucet.program_owner, default_owner,
|
||||
"faucet must have a non-default program_owner"
|
||||
);
|
||||
|
||||
let bridge = state.get_account_by_id(bridge_id);
|
||||
assert_ne!(
|
||||
bridge.program_owner, default_owner,
|
||||
"bridge must have a non-default program_owner"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_commitment_set_digest_differs_from_empty_state() {
|
||||
// The genesis state inserts DUMMY_COMMITMENT, so its commitment-set digest
|
||||
// must differ from a freshly-created empty state's all-zero root. Catches
|
||||
// the mutation that replaces `commitment_set_digest` with `Default::default()`.
|
||||
let genesis = V03State::new_with_genesis_accounts(&[], vec![], 0);
|
||||
let empty = V03State::new();
|
||||
assert_ne!(
|
||||
genesis.commitment_set_digest(),
|
||||
empty.commitment_set_digest()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_genesis() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
@ -1376,6 +1418,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -1388,7 +1435,6 @@ pub mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender_keys.account_id()],
|
||||
vec![sender_nonce],
|
||||
vec![(recipient_keys.npk(), recipient_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
@ -1429,6 +1475,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: epk_1,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret_1,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -1437,6 +1488,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: epk_2,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: shared_secret_2,
|
||||
identifier: 0,
|
||||
@ -1446,16 +1502,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(sender_keys.npk(), sender_keys.vpk(), epk_1),
|
||||
(recipient_keys.npk(), recipient_keys.vpk(), epk_2),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -1494,6 +1541,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -1507,13 +1559,8 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![*recipient_account_id],
|
||||
vec![],
|
||||
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -1630,6 +1677,79 @@ pub mod tests {
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
}
|
||||
|
||||
fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: Program::authenticated_transfer_program().id(),
|
||||
balance: 100,
|
||||
nonce: Nonce(0xdead_beef),
|
||||
..Account::default()
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
let state = V03State::new_with_genesis_accounts(&[], vec![], 0)
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
let tx = private_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys,
|
||||
37,
|
||||
&state,
|
||||
);
|
||||
(state, tx)
|
||||
}
|
||||
|
||||
/// After a valid fully-private tx is proven, tampering with a note's epk should
|
||||
/// make the shielding proof invalid.
|
||||
#[test]
|
||||
fn privacy_tampered_epk_is_rejected() {
|
||||
use crate::validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
let (state, mut tx) = valid_private_transfer_tx_and_state();
|
||||
|
||||
// Baseline: the untampered tx verifies
|
||||
assert!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(),
|
||||
"the unmodified private transfer must verify"
|
||||
);
|
||||
|
||||
// Flip a byte of the first note's epk
|
||||
tx.message.encrypted_private_post_states[0].epk.0[0] ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0),
|
||||
Err(LeeError::InvalidPrivacyPreservingProof)
|
||||
),
|
||||
"a tampered epk must be rejected by proof verification"
|
||||
);
|
||||
}
|
||||
|
||||
/// After a valid fully-private tx is proven, tampering with a note's view tag should
|
||||
/// make the shielding proof invalid.
|
||||
#[test]
|
||||
fn privacy_tampered_view_tag_is_rejected() {
|
||||
use crate::validated_state_diff::ValidatedStateDiff;
|
||||
|
||||
let (state, mut tx) = valid_private_transfer_tx_and_state();
|
||||
|
||||
// Baseline: the untampered tx verifies.
|
||||
assert!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0).is_ok(),
|
||||
"the unmodified private transfer must verify"
|
||||
);
|
||||
|
||||
// Flip the first note's view_tag
|
||||
tx.message.encrypted_private_post_states[0].view_tag ^= 0xFF;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0),
|
||||
Err(LeeError::InvalidPrivacyPreservingProof)
|
||||
),
|
||||
"a tampered view_tag must be rejected by proof verification"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_deshielded() {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
@ -1992,6 +2112,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2003,6 +2128,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2048,6 +2178,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2059,6 +2194,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2104,6 +2244,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2115,6 +2260,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2160,6 +2310,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2171,6 +2326,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2216,6 +2376,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2227,6 +2392,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2270,6 +2440,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&sender_keys.vpk(),
|
||||
&[0_u8; 32],
|
||||
@ -2281,6 +2456,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
npk: recipient_keys.npk(),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(
|
||||
&recipient_keys.vpk(),
|
||||
@ -2326,6 +2506,8 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2359,6 +2541,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2370,7 +2554,7 @@ pub mod tests {
|
||||
let (output, _proof) = result.expect("private PDA claim should succeed");
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.ciphertexts.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
assert!(output.public_post_states.is_empty());
|
||||
}
|
||||
@ -2400,6 +2584,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk_b, &keys_b.vpk()),
|
||||
npk: npk_b,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2437,6 +2623,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((seed, seed, callee_id)).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2477,6 +2665,8 @@ pub mod tests {
|
||||
vec![pre_state],
|
||||
Program::serialize_instruction((claim_seed, wrong_delegated_seed, callee_id)).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2516,12 +2706,16 @@ pub mod tests {
|
||||
Program::serialize_instruction(seed).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys_a.npk(), &keys_a.vpk()),
|
||||
npk: keys_a.npk(),
|
||||
ssk: shared_a,
|
||||
identifier: u128::MAX,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys_b.npk(), &keys_b.vpk()),
|
||||
npk: keys_b.npk(),
|
||||
ssk: shared_b,
|
||||
identifier: u128::MAX,
|
||||
@ -2564,6 +2758,8 @@ pub mod tests {
|
||||
vec![owned_pre_state],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret,
|
||||
identifier: u128::MAX,
|
||||
@ -2652,12 +2848,22 @@ pub mod tests {
|
||||
Program::serialize_instruction(100_u128).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (1, vec![]),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: (1, vec![]),
|
||||
@ -3003,6 +3209,11 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: sender_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3016,13 +3227,9 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_account_id],
|
||||
vec![Nonce(0)],
|
||||
vec![(sender_keys.npk(), sender_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_account_id], vec![Nonce(0)], output)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_private_key]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -3129,6 +3336,11 @@ pub mod tests {
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: to_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&to_keys.npk(),
|
||||
&to_keys.vpk(),
|
||||
),
|
||||
ssk: to_ss,
|
||||
nsk: from_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3137,6 +3349,11 @@ pub mod tests {
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: from_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&from_keys.npk(),
|
||||
&from_keys.vpk(),
|
||||
),
|
||||
ssk: from_ss,
|
||||
nsk: to_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3149,16 +3366,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
(to_keys.npk(), to_keys.vpk(), to_epk),
|
||||
(from_keys.npk(), from_keys.vpk(), from_epk),
|
||||
],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let transaction = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
@ -3406,6 +3614,11 @@ pub mod tests {
|
||||
vec![authorized_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3415,13 +3628,7 @@ pub mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create message from circuit output
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
@ -3454,6 +3661,11 @@ pub mod tests {
|
||||
vec![unauthorized_account],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
npk: private_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3462,13 +3674,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -3506,6 +3712,11 @@ pub mod tests {
|
||||
vec![authorized_account.clone()],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3514,13 +3725,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(private_keys.npk(), private_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
@ -3553,6 +3758,11 @@ pub mod tests {
|
||||
vec![account_metadata],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&private_keys.npk(),
|
||||
&private_keys.vpk(),
|
||||
),
|
||||
ssk: shared_secret2,
|
||||
nsk: private_keys.nsk,
|
||||
identifier: 0,
|
||||
@ -3630,6 +3840,11 @@ pub mod tests {
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
|
||||
.0,
|
||||
nsk: sender_keys.nsk,
|
||||
@ -3657,6 +3872,11 @@ pub mod tests {
|
||||
vec![private_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&sender_keys.npk(),
|
||||
&sender_keys.vpk(),
|
||||
),
|
||||
ssk: SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0)
|
||||
.0,
|
||||
nsk: sender_keys.nsk,
|
||||
@ -3718,6 +3938,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&recipient_keys.npk(),
|
||||
&recipient_keys.vpk(),
|
||||
),
|
||||
ssk: recipient,
|
||||
nsk: recipient_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -3872,6 +4097,11 @@ pub mod tests {
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3880,13 +4110,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(account_keys.npk(), account_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
@ -3941,6 +4165,11 @@ pub mod tests {
|
||||
vec![pre],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&account_keys.npk(),
|
||||
&account_keys.vpk(),
|
||||
),
|
||||
npk: account_keys.npk(),
|
||||
ssk: shared_secret,
|
||||
identifier: 0,
|
||||
@ -3949,13 +4178,7 @@ pub mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![],
|
||||
vec![],
|
||||
vec![(account_keys.npk(), account_keys.vpk(), epk)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(vec![], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
@ -4504,6 +4727,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: alice_epk_0.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_0,
|
||||
identifier: 0,
|
||||
@ -4513,13 +4741,9 @@ pub mod tests {
|
||||
&auth_transfer.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_0.clone())],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4544,6 +4768,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: alice_epk_1.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
npk: alice_npk,
|
||||
ssk: alice_shared_1,
|
||||
identifier: 1,
|
||||
@ -4553,13 +4782,9 @@ pub mod tests {
|
||||
&auth_transfer.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![funder_id],
|
||||
vec![funder_nonce],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_1.clone())],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![funder_id], vec![funder_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&funder_keys.signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4587,6 +4812,11 @@ pub mod tests {
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: alice_epk_0,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
ssk: alice_shared_0,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -4600,13 +4830,9 @@ pub mod tests {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![Nonce(0)],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_0)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![Nonce(0)], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4628,6 +4854,11 @@ pub mod tests {
|
||||
Program::serialize_instruction((seed, amount, auth_transfer_id)).unwrap(),
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: alice_epk_1,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
ssk: alice_shared_1,
|
||||
nsk: alice_keys.nsk,
|
||||
membership_proof: state
|
||||
@ -4641,13 +4872,8 @@ pub mod tests {
|
||||
&spend_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![],
|
||||
vec![(alice_npk, alice_keys.vpk(), alice_epk_1)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![], output).unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
@ -4690,6 +4916,11 @@ pub mod tests {
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&alice_npk,
|
||||
&alice_keys.vpk(),
|
||||
),
|
||||
nsk: alice_keys.nsk,
|
||||
ssk: alice_shared_1_refund,
|
||||
membership_proof: state
|
||||
@ -4702,17 +4933,9 @@ pub mod tests {
|
||||
&Program::authenticated_transfer_program().into(),
|
||||
)
|
||||
.unwrap();
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![recipient_id],
|
||||
vec![recipient_nonce],
|
||||
vec![(
|
||||
alice_npk,
|
||||
alice_keys.vpk(),
|
||||
EphemeralPublicKey(vec![12_u8; 1088]),
|
||||
)],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![recipient_id], vec![recipient_nonce], output)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&recipient_signing_key]);
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(
|
||||
|
||||
@ -492,12 +492,7 @@ fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
ciphertexts: message
|
||||
.encrypted_private_post_states
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|value| value.ciphertext)
|
||||
.collect(),
|
||||
encrypted_private_post_states: message.encrypted_private_post_states.clone(),
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers: message.new_nullifiers.clone(),
|
||||
block_validity_window: message.block_validity_window,
|
||||
@ -526,6 +521,44 @@ mod tests {
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn public_diff_reflects_a_successful_transfer() {
|
||||
// A successful native transfer must record the debited sender in
|
||||
// `public_diff()`. Catches the mutation that replaces `public_diff` with
|
||||
// `HashMap::new()` (which would hide every account change).
|
||||
use authenticated_transfer_core::Instruction as AtInstruction;
|
||||
|
||||
let from_key = PrivateKey::try_new([1_u8; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let to_key = PrivateKey::try_new([2_u8; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
|
||||
let state = V03State::new_with_genesis_accounts(&[(from, 100)], vec![], 0);
|
||||
let program_id = Program::authenticated_transfer_program().id();
|
||||
let message = Message::try_new(
|
||||
program_id,
|
||||
vec![from, to],
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
AtInstruction::Transfer { amount: 5 },
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = crate::PublicTransaction::new(message, witness_set);
|
||||
|
||||
let diff = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0)
|
||||
.expect("a valid native transfer must validate");
|
||||
let public_diff = diff.public_diff();
|
||||
|
||||
assert!(
|
||||
public_diff.contains_key(&from),
|
||||
"public_diff must contain the debited sender",
|
||||
);
|
||||
assert_eq!(
|
||||
public_diff[&from].balance, 95,
|
||||
"sender balance in the diff must reflect the debit",
|
||||
);
|
||||
}
|
||||
|
||||
/// Privacy-path version of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the victim's balance is left untouched.
|
||||
///
|
||||
@ -542,7 +575,7 @@ mod tests {
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity, SharedSecretKey,
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -626,6 +659,11 @@ mod tests {
|
||||
// [2] recipient — first seen in authenticated_transfer's program_output.pre_states
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: attacker_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&attacker_keys.npk(),
|
||||
&attacker_keys.vpk(),
|
||||
),
|
||||
ssk: attacker_ssk,
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
@ -650,7 +688,6 @@ mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
@ -690,7 +727,7 @@ mod tests {
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
use lee_core::{
|
||||
Commitment, InputAccountIdentity, SharedSecretKey,
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
@ -782,6 +819,11 @@ mod tests {
|
||||
// so PrivateAuthorizedUpdate is not an option.
|
||||
let account_identities = vec![
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: attacker_epk,
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&attacker_keys.npk(),
|
||||
&attacker_keys.vpk(),
|
||||
),
|
||||
ssk: attacker_ssk,
|
||||
nsk: attacker_keys.nsk,
|
||||
membership_proof,
|
||||
@ -807,7 +849,6 @@ mod tests {
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
vec![(attacker_keys.npk(), attacker_keys.vpk(), attacker_epk)],
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -53,3 +53,33 @@ impl From<BasicAuth> for BasicAuthCredentials {
|
||||
Self::new(value.username, value.password)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use super::BasicAuth;
|
||||
|
||||
#[test]
|
||||
fn parse_preserves_non_empty_password() {
|
||||
let auth = BasicAuth::from_str("user:secret").expect("must parse");
|
||||
assert_eq!(auth.username, "user");
|
||||
assert_eq!(auth.password.as_deref(), Some("secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_password_is_none() {
|
||||
// A trailing colon means an empty password, which must become `None`.
|
||||
// Catches deletion of `!` in `.filter(|p| !p.is_empty())`, which would
|
||||
// instead yield `Some("")`.
|
||||
let auth = BasicAuth::from_str("user:").expect("must parse");
|
||||
assert_eq!(auth.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_username_only_has_no_password() {
|
||||
let auth = BasicAuth::from_str("alice").expect("must parse");
|
||||
assert_eq!(auth.username, "alice");
|
||||
assert_eq!(auth.password, None);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,4 +93,16 @@ mod tests {
|
||||
let deserialized = HashType::from_str(&serialized).unwrap();
|
||||
assert_eq!(original, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_ref_returns_exact_inner_bytes() {
|
||||
// `HashType::as_ref` must return exactly the inner `[u8; 32]` — not an
|
||||
// empty slice or a placeholder. Catches mutations of `as_ref` that return
|
||||
// `Vec::leak(Vec::new())`, `vec![0]`, or `vec![1]`.
|
||||
let known = [0x42_u8; 32];
|
||||
let hash = HashType(known);
|
||||
assert_eq!(hash.as_ref(), &known);
|
||||
assert_eq!(hash.as_ref().len(), 32);
|
||||
assert_eq!(HashType([0_u8; 32]).as_ref().len(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,18 +78,9 @@ impl LeeTransaction {
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<ValidatedStateDiff, lee::error::LeeError> {
|
||||
let diff = match self {
|
||||
Self::Public(tx) => {
|
||||
ValidatedStateDiff::from_public_transaction(tx, state, block_id, timestamp)
|
||||
}
|
||||
Self::PrivacyPreserving(tx) => ValidatedStateDiff::from_privacy_preserving_transaction(
|
||||
tx, state, block_id, timestamp,
|
||||
),
|
||||
Self::ProgramDeployment(tx) => {
|
||||
ValidatedStateDiff::from_program_deployment_transaction(tx, state)
|
||||
}
|
||||
}?;
|
||||
let diff = self.compute_state_diff(state, block_id, timestamp)?;
|
||||
|
||||
// system accounts guard
|
||||
let system_accounts = lee::CLOCK_PROGRAM_ACCOUNT_IDS.iter().copied().chain([
|
||||
lee::system_faucet_account_id(),
|
||||
lee::system_bridge_account_id(),
|
||||
@ -101,6 +92,28 @@ impl LeeTransaction {
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
/// Computes the validated state diff without enforcing the system-account
|
||||
/// restriction. Shared by [`Self::validate_on_state`] and
|
||||
/// [`Self::execute_without_system_accounts_check_on_state`].
|
||||
fn compute_state_diff(
|
||||
&self,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<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
|
||||
/// system accounts, and applies the resulting diff to the state.
|
||||
pub fn execute_check_on_state(
|
||||
@ -115,6 +128,28 @@ impl LeeTransaction {
|
||||
state.apply_state_diff(diff);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Similar to [`Self::execute_check_on_state`], but skips the system-account guard.
|
||||
///
|
||||
/// FIXME: HOT FIX (testnet v0.2): the indexer replays blocks the sequencer already
|
||||
/// accepted, including sequencer-generated deposit transactions that
|
||||
/// legitimately modify the bridge account. The `TransactionOrigin::Sequencer`
|
||||
/// tag that lets the sequencer bypass the guard is not carried in the block,
|
||||
/// so the indexer cannot yet distinguish deposit txs from user txs.
|
||||
///
|
||||
/// REMOVE ME when the indexer can authenticate deposit transactions.
|
||||
pub fn execute_without_system_accounts_check_on_state(
|
||||
self,
|
||||
state: &mut V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<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 {
|
||||
@ -188,3 +223,47 @@ fn validate_doesnt_modify_account(
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee::{
|
||||
AccountId, CLOCK_01_PROGRAM_ACCOUNT_ID, PrivateKey, PublicKey, V03State,
|
||||
system_bridge_account_id, system_faucet_account_id,
|
||||
};
|
||||
|
||||
use crate::test_utils::create_transaction_native_token_transfer;
|
||||
|
||||
#[test]
|
||||
fn system_account_ids_are_distinct_and_non_default() {
|
||||
let faucet = system_faucet_account_id();
|
||||
let bridge = system_bridge_account_id();
|
||||
assert_ne!(faucet, AccountId::default());
|
||||
assert_ne!(bridge, AccountId::default());
|
||||
assert_ne!(faucet, bridge);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_on_state_rejects_modifying_a_system_account() {
|
||||
// A native transfer that credits a clock system account *changes* that
|
||||
// account, so `validate_doesnt_modify_account` must reject it. Catches
|
||||
// the `!=` → `==` inversion at `validate_doesnt_modify_account` (a changed
|
||||
// account would no longer be flagged) and `public_diff → HashMap::new()`
|
||||
// (an empty diff hides the modification).
|
||||
let sender_key = PrivateKey::try_new([5_u8; 32]).expect("valid key");
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let state = V03State::new_with_genesis_accounts(&[(sender_id, 10_000)], vec![], 0);
|
||||
|
||||
let tx = create_transaction_native_token_transfer(
|
||||
sender_id,
|
||||
0,
|
||||
CLOCK_01_PROGRAM_ACCOUNT_ID,
|
||||
100,
|
||||
&sender_key,
|
||||
);
|
||||
|
||||
assert!(
|
||||
tx.validate_on_state(&state, 1, 0).is_err(),
|
||||
"validate_on_state must reject a transfer that credits a clock system account",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +171,10 @@ impl IndexerStore {
|
||||
transaction
|
||||
.clone()
|
||||
.transaction_stateless_check()?
|
||||
.execute_check_on_state(
|
||||
// FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to
|
||||
// sequencer-generated deposit tx'es;
|
||||
// CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions
|
||||
.execute_without_system_accounts_check_on_state(
|
||||
&mut state_guard,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
|
||||
Binary file not shown.
@ -7,8 +7,9 @@ use zeroize::Zeroizing;
|
||||
|
||||
pub mod python_path;
|
||||
|
||||
/// NSK and VSK as fixed-length zeroizing byte arrays.
|
||||
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>);
|
||||
/// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte
|
||||
/// arrays.
|
||||
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 64]>);
|
||||
|
||||
// TODO: encrypt at rest alongside broader wallet storage encryption work.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -123,7 +124,7 @@ impl KeycardWallet {
|
||||
}
|
||||
|
||||
pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PyResult<PublicKey> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
@ -190,7 +191,7 @@ impl KeycardWallet {
|
||||
path: &str,
|
||||
message: &[u8; 32],
|
||||
) -> PyResult<(Signature, PublicKey)> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
@ -239,13 +240,13 @@ impl KeycardWallet {
|
||||
};
|
||||
|
||||
let vsk = {
|
||||
if raw_vsk.len() != 32 {
|
||||
if raw_vsk.len() != 64 {
|
||||
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
|
||||
"expected 32-byte VSK from keycard, got {} bytes",
|
||||
"expected 64-byte VSK from keycard, got {} bytes",
|
||||
raw_vsk.len()
|
||||
)));
|
||||
}
|
||||
let mut arr = Zeroizing::new([0_u8; 32]);
|
||||
let mut arr = Zeroizing::new([0_u8; 64]);
|
||||
arr.copy_from_slice(&raw_vsk);
|
||||
arr
|
||||
};
|
||||
@ -257,7 +258,7 @@ impl KeycardWallet {
|
||||
pin: &str,
|
||||
path: &str,
|
||||
) -> PyResult<PrivateKeyPair> {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = Self::new(py)?;
|
||||
wallet.connect(py, pin)?;
|
||||
|
||||
@ -48,7 +48,7 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> {
|
||||
|
||||
let sys = PyModule::import(py, "sys")?;
|
||||
let binding = sys.getattr("path")?;
|
||||
let sys_path = binding.downcast::<PyList>()?;
|
||||
let sys_path = binding.cast::<PyList>()?;
|
||||
|
||||
for path in &paths_to_add {
|
||||
let path_str = path.to_str().expect("Invalid path");
|
||||
|
||||
@ -208,7 +208,10 @@ impl RocksDBIO {
|
||||
"transaction pre check failed with err {err:?}"
|
||||
))
|
||||
})?
|
||||
.execute_check_on_state(
|
||||
// FIXME: HOT FIX (testnet v0.2): does not check for system account updates due to
|
||||
// sequencer-generated deposit tx'es;
|
||||
// CHANGE ME back to `execute_check_on_state` when the indexer can authenticate deposit transactions
|
||||
.execute_without_system_accounts_check_on_state(
|
||||
&mut breakpoint,
|
||||
block.header.block_id,
|
||||
block.header.timestamp,
|
||||
|
||||
@ -8,7 +8,7 @@ use lee_core::{
|
||||
Identifier, InputAccountIdentity, MembershipProof, NullifierPublicKey, NullifierSecretKey,
|
||||
SharedSecretKey,
|
||||
account::{AccountWithMetadata, Nonce},
|
||||
encryption::{EphemeralPublicKey, ViewingPublicKey},
|
||||
encryption::{EncryptedAccountData, EphemeralPublicKey, ViewingPublicKey},
|
||||
};
|
||||
|
||||
use crate::{ExecutionFailureKind, WalletCore};
|
||||
@ -169,10 +169,7 @@ impl AccountIdentity {
|
||||
}
|
||||
|
||||
pub struct PrivateAccountKeys {
|
||||
pub npk: NullifierPublicKey,
|
||||
pub ssk: SharedSecretKey,
|
||||
pub vpk: ViewingPublicKey,
|
||||
pub epk: EphemeralPublicKey,
|
||||
}
|
||||
|
||||
enum State {
|
||||
@ -376,12 +373,7 @@ impl AccountManager {
|
||||
self.states
|
||||
.iter()
|
||||
.filter_map(|state| match state {
|
||||
State::Private(pre) => Some(PrivateAccountKeys {
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
vpk: pre.vpk.clone(),
|
||||
epk: pre.epk.clone(),
|
||||
}),
|
||||
State::Private(pre) => Some(PrivateAccountKeys { ssk: pre.ssk }),
|
||||
State::Public { .. } | State::PublicKeycard { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
@ -398,6 +390,8 @@ impl AccountManager {
|
||||
State::Public { .. } | State::PublicKeycard { .. } => InputAccountIdentity::Public,
|
||||
State::Private(pre) if pre.is_pda => match (pre.nsk, pre.proof.clone()) {
|
||||
(Some(nsk), Some(membership_proof)) => InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk: pre.epk.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
|
||||
ssk: pre.ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
@ -405,6 +399,8 @@ impl AccountManager {
|
||||
seed: None,
|
||||
},
|
||||
_ => InputAccountIdentity::PrivatePdaInit {
|
||||
epk: pre.epk.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
identifier: pre.identifier,
|
||||
@ -414,6 +410,8 @@ impl AccountManager {
|
||||
State::Private(pre) => match (pre.nsk, pre.proof.clone()) {
|
||||
(Some(nsk), Some(membership_proof)) => {
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk: pre.epk.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
|
||||
ssk: pre.ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
@ -421,11 +419,15 @@ impl AccountManager {
|
||||
}
|
||||
}
|
||||
(Some(nsk), None) => InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: pre.epk.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
|
||||
ssk: pre.ssk,
|
||||
nsk,
|
||||
identifier: pre.identifier,
|
||||
},
|
||||
(None, _) => InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: pre.epk.clone(),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&pre.npk, &pre.vpk),
|
||||
npk: pre.npk,
|
||||
ssk: pre.ssk,
|
||||
identifier: pre.identifier,
|
||||
@ -479,7 +481,7 @@ impl AccountManager {
|
||||
.collect();
|
||||
|
||||
if let Some(pin) = self.pin.clone() {
|
||||
pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> {
|
||||
pyo3::Python::attach(|py| -> pyo3::PyResult<()> {
|
||||
python_path::add_python_path(py)?;
|
||||
let wallet = KeycardWallet::new(py)?;
|
||||
wallet.connect(py, &pin)?;
|
||||
|
||||
@ -39,7 +39,7 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
) -> Result<SubcommandReturnValue> {
|
||||
match self {
|
||||
Self::Available => {
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)
|
||||
.expect("`wallet::keycard::available`: unable to setup python path");
|
||||
|
||||
@ -61,7 +61,7 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
Self::Connect => {
|
||||
let pin = read_pin()?;
|
||||
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)
|
||||
.expect("`wallet::keycard::connect`: unable to setup python path");
|
||||
|
||||
@ -81,7 +81,7 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
Self::Disconnect => {
|
||||
let pin = read_pin()?;
|
||||
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)
|
||||
.expect("`wallet::keycard::disconnect`: unable to setup python path");
|
||||
|
||||
@ -105,7 +105,7 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
Self::Init => {
|
||||
let pin = read_pin()?;
|
||||
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)
|
||||
.expect("`wallet::keycard::init`: unable to setup python path");
|
||||
|
||||
@ -128,7 +128,7 @@ impl WalletSubcommand for KeycardSubcommand {
|
||||
let pin = read_pin()?;
|
||||
let mnemonic = read_mnemonic()?;
|
||||
|
||||
Python::with_gil(|py| {
|
||||
Python::attach(|py| {
|
||||
python_path::add_python_path(py)
|
||||
.expect("`wallet::keycard::load`: unable to setup python path");
|
||||
|
||||
|
||||
@ -587,10 +587,6 @@ impl WalletCore {
|
||||
lee::privacy_preserving_transaction::message::Message::try_from_circuit_output(
|
||||
acc_manager.public_account_ids(),
|
||||
acc_manager.public_account_nonces(),
|
||||
private_account_keys
|
||||
.iter()
|
||||
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
|
||||
.collect(),
|
||||
output,
|
||||
)?;
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use lee_core::{
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptionScheme, InputAccountIdentity,
|
||||
MembershipProof, Nullifier, NullifierPublicKey, NullifierSecretKey,
|
||||
PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
Commitment, CommitmentSetDigest, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, InputAccountIdentity, MembershipProof, Nullifier, NullifierPublicKey,
|
||||
NullifierSecretKey, PrivacyPreservingCircuitOutput, PrivateAccountKind, SharedSecretKey,
|
||||
account::{Account, AccountId, Nonce},
|
||||
compute_digest_for_path,
|
||||
};
|
||||
@ -17,7 +17,7 @@ pub fn compute_circuit_output(
|
||||
let mut output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: Vec::new(),
|
||||
public_post_states: Vec::new(),
|
||||
ciphertexts: Vec::new(),
|
||||
encrypted_private_post_states: Vec::new(),
|
||||
new_commitments: Vec::new(),
|
||||
new_nullifiers: Vec::new(),
|
||||
block_validity_window,
|
||||
@ -40,6 +40,8 @@ pub fn compute_circuit_output(
|
||||
output.public_post_states.push(post_state);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
nsk,
|
||||
identifier,
|
||||
@ -71,11 +73,15 @@ pub fn compute_circuit_output(
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateAuthorizedUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
@ -105,11 +111,15 @@ pub fn compute_circuit_output(
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk,
|
||||
view_tag,
|
||||
npk,
|
||||
ssk,
|
||||
identifier,
|
||||
@ -140,11 +150,15 @@ pub fn compute_circuit_output(
|
||||
&account_id,
|
||||
&PrivateAccountKind::Regular(*identifier),
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk,
|
||||
view_tag,
|
||||
npk: _,
|
||||
ssk,
|
||||
identifier,
|
||||
@ -187,11 +201,15 @@ pub fn compute_circuit_output(
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
}
|
||||
InputAccountIdentity::PrivatePdaUpdate {
|
||||
epk,
|
||||
view_tag,
|
||||
ssk,
|
||||
nsk,
|
||||
membership_proof,
|
||||
@ -231,6 +249,8 @@ pub fn compute_circuit_output(
|
||||
identifier: *identifier,
|
||||
},
|
||||
ssk,
|
||||
epk,
|
||||
*view_tag,
|
||||
new_nullifier,
|
||||
new_nonce,
|
||||
);
|
||||
@ -243,7 +263,7 @@ pub fn compute_circuit_output(
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "All seven inputs are distinct concerns from the variant arms; bundling would be artificial"
|
||||
reason = "Inputs are distinct concerns from the variant arms; bundling would be artificial"
|
||||
)]
|
||||
fn emit_private_output(
|
||||
output: &mut PrivacyPreservingCircuitOutput,
|
||||
@ -252,6 +272,8 @@ fn emit_private_output(
|
||||
account_id: &AccountId,
|
||||
kind: &PrivateAccountKind,
|
||||
shared_secret: &SharedSecretKey,
|
||||
epk: &EphemeralPublicKey,
|
||||
view_tag: u8,
|
||||
new_nullifier: (Nullifier, CommitmentSetDigest),
|
||||
new_nonce: Nonce,
|
||||
) {
|
||||
@ -270,7 +292,13 @@ fn emit_private_output(
|
||||
);
|
||||
|
||||
output.new_commitments.push(commitment_post);
|
||||
output.ciphertexts.push(encrypted_account);
|
||||
output
|
||||
.encrypted_private_post_states
|
||||
.push(EncryptedAccountData {
|
||||
ciphertext: encrypted_account,
|
||||
epk: epk.clone(),
|
||||
view_tag,
|
||||
});
|
||||
*output_index = output_index
|
||||
.checked_add(1)
|
||||
.unwrap_or_else(|| panic!("Too many private accounts, output index overflow"));
|
||||
|
||||
@ -9,11 +9,14 @@
|
||||
|
||||
#![expect(
|
||||
clippy::arithmetic_side_effects,
|
||||
clippy::as_conversions,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::float_arithmetic,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::non_ascii_literal,
|
||||
clippy::print_stderr,
|
||||
clippy::print_stdout,
|
||||
clippy::suboptimal_flops,
|
||||
reason = "Bench tool: matches test-style fixture code"
|
||||
)]
|
||||
|
||||
@ -68,6 +71,13 @@ struct BenchResult {
|
||||
user_cycles: u64,
|
||||
segments: usize,
|
||||
exec_stats: Stats,
|
||||
/// Compute-only execution time (ms): best-of-N executor wall-time minus the calibrated
|
||||
/// host-side fixed per-call overhead. Filled after the calibration fit over all cases.
|
||||
net_compute_ms: Option<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.
|
||||
/// Single-sample (n=1) when --prove is on without explicit repetition, since proving is slow.
|
||||
prove_stats: Option<Stats>,
|
||||
@ -81,6 +91,89 @@ struct BenchResult {
|
||||
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 {
|
||||
program: &'static str,
|
||||
instruction_label: &'static str,
|
||||
@ -185,6 +278,8 @@ impl Case {
|
||||
user_cycles: info.cycles(),
|
||||
segments: info.segments.len(),
|
||||
exec_stats,
|
||||
net_compute_ms: None,
|
||||
calibrated_ms: None,
|
||||
prove_stats,
|
||||
prove_total_cycles,
|
||||
prove_user_cycles,
|
||||
@ -495,12 +590,23 @@ fn main() -> Result<()> {
|
||||
)?,
|
||||
];
|
||||
|
||||
let results: Vec<BenchResult> = cases
|
||||
let mut results: Vec<BenchResult> = cases
|
||||
.into_iter()
|
||||
.map(|c| c.run(prove, exec_iters))
|
||||
.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);
|
||||
if let Some(cal) = calibration {
|
||||
print_calibration(&cal);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ppe")]
|
||||
let ppe_results = if cli.ppe { ppe::run_all() } else { Vec::new() };
|
||||
@ -525,6 +631,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let combined = serde_json::json!({
|
||||
"standalone": results,
|
||||
"calibration": calibration,
|
||||
"ppe": ppe_results,
|
||||
});
|
||||
std::fs::write(&out_path, serde_json::to_string_pretty(&combined)?)?;
|
||||
@ -533,6 +640,24 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_calibration(cal: &Calibration) {
|
||||
println!("\npublic-execution ms calibration (pinned hardware):");
|
||||
println!(
|
||||
" fit: best_ms = {:.4} + {:.3e} * user_cycles (n={}, R²={:.4})",
|
||||
cal.intercept_ms, cal.slope_ms_per_cycle, cal.n, cal.r2,
|
||||
);
|
||||
println!(
|
||||
" throughput: {:.0} cycles/ms",
|
||||
cal.throughput_cycles_per_ms,
|
||||
);
|
||||
println!(
|
||||
" fixed overhead: {:.3} ms host-side per call (ELF parse + env build, off-cycle)",
|
||||
cal.intercept_ms,
|
||||
);
|
||||
println!(" calib_ms = user_cycles / throughput (compute only, overhead excluded)");
|
||||
println!(" net_ms = best exec_ms - fixed overhead (measured compute, overhead stripped)");
|
||||
}
|
||||
|
||||
fn print_table(results: &[BenchResult], prove: bool) {
|
||||
let pw = results
|
||||
.iter()
|
||||
@ -555,15 +680,28 @@ fn print_table(results: &[BenchResult], prove: bool) {
|
||||
.unwrap_or(0)
|
||||
.max("exec_ms (best / mean ± stdev)".len());
|
||||
|
||||
let dw = 10_usize;
|
||||
println!(
|
||||
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$}",
|
||||
"program", "instruction", "user_cycles", "segments", "exec_ms (best / mean ± stdev)",
|
||||
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$} {:>dw$} {:>dw$}",
|
||||
"program",
|
||||
"instruction",
|
||||
"user_cycles",
|
||||
"segments",
|
||||
"exec_ms (best / mean ± stdev)",
|
||||
"calib_ms",
|
||||
"net_ms",
|
||||
);
|
||||
println!("{}", "-".repeat(pw + iw + cw + sw + exec_w + 8));
|
||||
println!("{}", "-".repeat(pw + iw + cw + sw + exec_w + 2 * dw + 12));
|
||||
for r in results {
|
||||
let calib = r
|
||||
.calibrated_ms
|
||||
.map_or_else(|| "-".to_owned(), |v| format!("{v:.2}"));
|
||||
let net = r
|
||||
.net_compute_ms
|
||||
.map_or_else(|| "-".to_owned(), |v| format!("{v:.2}"));
|
||||
println!(
|
||||
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$}",
|
||||
r.program, r.instruction, r.user_cycles, r.segments, r.exec_stats,
|
||||
"{:<pw$} {:<iw$} {:>cw$} {:>sw$} {:<exec_w$} {:>dw$} {:>dw$}",
|
||||
r.program, r.instruction, r.user_cycles, r.segments, r.exec_stats, calib, net,
|
||||
);
|
||||
}
|
||||
|
||||
@ -595,3 +733,78 @@ fn print_table(results: &[BenchResult], prove: bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cycle_bench::stats::Stats;
|
||||
|
||||
use super::{BenchResult, Calibration};
|
||||
|
||||
/// Minimal `BenchResult` carrying only the fields the calibration fit reads:
|
||||
/// `user_cycles` (x) and `exec_stats.best_ms` (y).
|
||||
fn point(user_cycles: u64, best_ms: f64) -> BenchResult {
|
||||
BenchResult {
|
||||
program: "test",
|
||||
instruction: "test",
|
||||
user_cycles,
|
||||
segments: 1,
|
||||
exec_stats: Stats::from_samples(&[best_ms]),
|
||||
net_compute_ms: None,
|
||||
calibrated_ms: None,
|
||||
prove_stats: None,
|
||||
prove_total_cycles: None,
|
||||
prove_user_cycles: None,
|
||||
prove_paging_cycles: None,
|
||||
prove_segments: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn close(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < 1e-9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_recovers_a_known_line() {
|
||||
// best_ms = 10 + 0.001 * user_cycles -> slope 1e-3, intercept 10, throughput 1000.
|
||||
let results = [point(1000, 11.0), point(2000, 12.0), point(3000, 13.0)];
|
||||
let cal = Calibration::fit(&results).expect("fit over three points");
|
||||
|
||||
assert!(
|
||||
close(cal.slope_ms_per_cycle, 0.001),
|
||||
"slope {}",
|
||||
cal.slope_ms_per_cycle
|
||||
);
|
||||
assert!(
|
||||
close(cal.intercept_ms, 10.0),
|
||||
"intercept {}",
|
||||
cal.intercept_ms
|
||||
);
|
||||
assert!(
|
||||
close(cal.throughput_cycles_per_ms, 1000.0),
|
||||
"throughput {}",
|
||||
cal.throughput_cycles_per_ms,
|
||||
);
|
||||
assert!(close(cal.r2, 1.0), "r2 {}", cal.r2);
|
||||
assert_eq!(cal.n, 3);
|
||||
// calibrated_ms is the overhead-excluded compute prediction: slope * cycles.
|
||||
assert!(
|
||||
close(cal.calibrated_ms(2000), 2.0),
|
||||
"calib {}",
|
||||
cal.calibrated_ms(2000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_needs_at_least_two_points() {
|
||||
assert!(Calibration::fit(&[]).is_none());
|
||||
assert!(Calibration::fit(&[point(1000, 11.0)]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_with_identical_cycle_counts_returns_none() {
|
||||
// Zero spread in x leaves the slope undetermined; the fit must decline rather than divide
|
||||
// by zero.
|
||||
let results = [point(1000, 11.0), point(1000, 12.0)];
|
||||
assert!(Calibration::fit(&results).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user