mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-03 20:40:22 +00:00
refactor(lee::state_machine): clean up long functions and files (#535)
* refactor(lee): split large modules into directories and extract tests Split state.rs, program.rs, circuit.rs, validated_state_diff.rs, merkle_tree, and core/program.rs into module directories with separate test files. State tests are further split into themed files (genesis, authenticated_transfer, circuit, claiming, etc.). Extract authenticate_public_transaction_signers helper in validated_state_diff to remove duplicated authentication logic. * chore: rebuild artifacts
This commit is contained in:
parent
f786aca73e
commit
072ea3b066
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.
@ -230,7 +230,7 @@ impl ChainedCall {
|
||||
/// Represents the final state of an `Account` after a program execution.
|
||||
///
|
||||
/// A post state may optionally request that the executing program
|
||||
/// becomes the owner of the account (a “claim”). This is used to signal
|
||||
/// becomes the owner of the account (a "claim"). This is used to signal
|
||||
/// that the program intends to take ownership of the account.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[cfg_attr(any(feature = "host", test), derive(PartialEq, Eq))]
|
||||
@ -766,338 +766,4 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validity_window_unbounded_accepts_any_value() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_bounded_range_includes_from_excludes_to() {
|
||||
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(9));
|
||||
assert!(!w.is_valid_for(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_from_bound() {
|
||||
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_to_bound() {
|
||||
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(4));
|
||||
assert!(!w.is_valid_for(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_adjacent_bounds_are_invalid() {
|
||||
// [5, 5) is an empty range — from == to
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
|
||||
assert_eq!(w.start(), Some(3));
|
||||
assert_eq!(w.end(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_for_unbounded() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_empty_is_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_inverted_is_invalid() {
|
||||
let from = 10_u64;
|
||||
let to = 5_u64;
|
||||
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_range() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(10_u64..100)
|
||||
.unwrap();
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_from() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(10_u64..);
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_to() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(..100_u64);
|
||||
assert_eq!(output.block_validity_window.start(), None);
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_empty_range_fails() {
|
||||
let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(5_u64..5);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_without_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.required_claim().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_account_getter() {
|
||||
let mut account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let mut account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte
|
||||
/// ordering, or the underlying hash breaks this test.
|
||||
#[test]
|
||||
fn for_private_pda_matches_pinned_value() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
|
||||
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Two groups with different viewing keys at the same (program, seed) get different addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_npk() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk_a = NullifierPublicKey([3; 32]);
|
||||
let npk_b = NullifierPublicKey([4; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different seeds produce different addresses, even with the same program and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_seed() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed_a = PdaSeed::new([2; 32]);
|
||||
let seed_b = PdaSeed::new([5; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different programs produce different addresses, even with the same seed and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_program_id() {
|
||||
let program_id_a: ProgramId = [1; 8];
|
||||
let program_id_b: ProgramId = [9; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`,
|
||||
/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_identifier() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// A private PDA at the same (program, seed) has a different address than a public PDA,
|
||||
/// because the private formula uses a different prefix and includes npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_from_public_pda() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_header_round_trips() {
|
||||
let regular = PrivateAccountKind::Regular(42);
|
||||
let pda = PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: u128::MAX,
|
||||
};
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()),
|
||||
Some(regular)
|
||||
);
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()),
|
||||
Some(pda)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_unknown_discriminant_returns_none() {
|
||||
let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN];
|
||||
bytes[0] = 0xFF;
|
||||
assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_private_account_dispatches_correctly() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_with_seeds() {
|
||||
let caller: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
|
||||
let expected = AccountId::for_public_pda(&caller, &seed);
|
||||
assert!(result.contains(&expected));
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
/// With no caller (top-level call), the result is always empty.
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_no_caller_returns_empty() {
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(None, &[seed]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
333
lee/state_machine/core/src/program/tests.rs
Normal file
333
lee/state_machine/core/src/program/tests.rs
Normal file
@ -0,0 +1,333 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validity_window_unbounded_accepts_any_value() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_bounded_range_includes_from_excludes_to() {
|
||||
let w: ValidityWindow<u64> = (Some(5), Some(10)).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(9));
|
||||
assert!(!w.is_valid_for(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_from_bound() {
|
||||
let w: ValidityWindow<u64> = (Some(5), None).try_into().unwrap();
|
||||
assert!(!w.is_valid_for(4));
|
||||
assert!(w.is_valid_for(5));
|
||||
assert!(w.is_valid_for(u64::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_only_to_bound() {
|
||||
let w: ValidityWindow<u64> = (None, Some(5)).try_into().unwrap();
|
||||
assert!(w.is_valid_for(0));
|
||||
assert!(w.is_valid_for(4));
|
||||
assert!(!w.is_valid_for(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_adjacent_bounds_are_invalid() {
|
||||
// [5, 5) is an empty range — from == to
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(5), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_inverted_bounds_are_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from((Some(10), Some(5))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_match_construction() {
|
||||
let w: ValidityWindow<u64> = (Some(3), Some(7)).try_into().unwrap();
|
||||
assert_eq!(w.start(), Some(3));
|
||||
assert_eq!(w.end(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_getters_for_unbounded() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::new_unbounded();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range() {
|
||||
let w: ValidityWindow<u64> = ValidityWindow::try_from(5_u64..10).unwrap();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_empty_is_invalid() {
|
||||
assert!(ValidityWindow::<u64>::try_from(5_u64..5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_inverted_is_invalid() {
|
||||
let from = 10_u64;
|
||||
let to = 5_u64;
|
||||
assert!(ValidityWindow::<u64>::try_from(from..to).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_from() {
|
||||
let w: ValidityWindow<u64> = (5_u64..).into();
|
||||
assert_eq!(w.start(), Some(5));
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_to() {
|
||||
let w: ValidityWindow<u64> = (..10_u64).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_window_from_range_full() {
|
||||
let w: ValidityWindow<u64> = (..).into();
|
||||
assert_eq!(w.start(), None);
|
||||
assert_eq!(w.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_range() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(10_u64..100)
|
||||
.unwrap();
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_from() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(10_u64..);
|
||||
assert_eq!(output.block_validity_window.start(), Some(10));
|
||||
assert_eq!(output.block_validity_window.end(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_with_block_validity_window_range_to() {
|
||||
let output = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.with_block_validity_window(..100_u64);
|
||||
assert_eq!(output.block_validity_window.start(), None);
|
||||
assert_eq!(output.block_validity_window.end(), Some(100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_output_try_with_block_validity_window_empty_range_fails() {
|
||||
let result = ProgramOutput::new(DEFAULT_PROGRAM_ID, None, vec![], vec![], vec![])
|
||||
.try_with_block_validity_window(5_u64..5);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_with_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new_claimed(account.clone(), Claim::Authorized);
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert_eq!(account_post_state.required_claim(), Some(Claim::Authorized));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_new_without_claim_constructor() {
|
||||
let account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account, account_post_state.account);
|
||||
assert!(account_post_state.required_claim().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_state_account_getter() {
|
||||
let mut account = Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
balance: 1337,
|
||||
data: vec![0xde, 0xad, 0xbe, 0xef].try_into().unwrap(),
|
||||
nonce: 10_u128.into(),
|
||||
};
|
||||
|
||||
let mut account_post_state = AccountPostState::new(account.clone());
|
||||
|
||||
assert_eq!(account_post_state.account(), &account);
|
||||
assert_eq!(account_post_state.account_mut(), &mut account);
|
||||
}
|
||||
|
||||
// ---- AccountId::for_private_pda tests ----
|
||||
|
||||
/// Pins `AccountId::for_private_pda` against a hardcoded expected output for a specific
|
||||
/// `(program_id, seed, npk, identifier)` tuple. Any change to `PRIVATE_PDA_PREFIX`, byte
|
||||
/// ordering, or the underlying hash breaks this test.
|
||||
#[test]
|
||||
fn for_private_pda_matches_pinned_value() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = u128::MAX;
|
||||
let expected = AccountId::new([
|
||||
59, 239, 182, 97, 14, 220, 96, 115, 238, 133, 143, 33, 234, 82, 237, 255, 148, 110, 54,
|
||||
124, 98, 159, 245, 101, 146, 182, 150, 54, 37, 62, 25, 17,
|
||||
]);
|
||||
assert_eq!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
/// Two groups with different viewing keys at the same (program, seed) get different addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_npk() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk_a = NullifierPublicKey([3; 32]);
|
||||
let npk_b = NullifierPublicKey([4; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_a, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk_b, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different seeds produce different addresses, even with the same program and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_seed() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed_a = PdaSeed::new([2; 32]);
|
||||
let seed_b = PdaSeed::new([5; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed_a, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id, &seed_b, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different programs produce different addresses, even with the same seed and npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_program_id() {
|
||||
let program_id_a: ProgramId = [1; 8];
|
||||
let program_id_b: ProgramId = [9; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id_a, &seed, &npk, u128::MAX),
|
||||
AccountId::for_private_pda(&program_id_b, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different identifiers produce different addresses for the same `(program_id, seed, npk)`,
|
||||
/// confirming that each `(program_id, seed, npk)` tuple controls a family of 2^128 addresses.
|
||||
#[test]
|
||||
fn for_private_pda_differs_for_different_identifier() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 1),
|
||||
);
|
||||
assert_ne!(
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, 0),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX),
|
||||
);
|
||||
}
|
||||
|
||||
/// A private PDA at the same (program, seed) has a different address than a public PDA,
|
||||
/// because the private formula uses a different prefix and includes npk.
|
||||
#[test]
|
||||
fn for_private_pda_differs_from_public_pda() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let private_id = AccountId::for_private_pda(&program_id, &seed, &npk, u128::MAX);
|
||||
let public_id = AccountId::for_public_pda(&program_id, &seed);
|
||||
assert_ne!(private_id, public_id);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_header_round_trips() {
|
||||
let regular = PrivateAccountKind::Regular(42);
|
||||
let pda = PrivateAccountKind::Pda {
|
||||
program_id: [1_u32; 8],
|
||||
seed: PdaSeed::new([2_u8; 32]),
|
||||
identifier: u128::MAX,
|
||||
};
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(®ular.to_header_bytes()),
|
||||
Some(regular)
|
||||
);
|
||||
assert_eq!(
|
||||
PrivateAccountKind::from_header_bytes(&pda.to_header_bytes()),
|
||||
Some(pda)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
#[test]
|
||||
fn private_account_kind_unknown_discriminant_returns_none() {
|
||||
let mut bytes = [0_u8; PrivateAccountKind::HEADER_LEN];
|
||||
bytes[0] = 0xFF;
|
||||
assert_eq!(PrivateAccountKind::from_header_bytes(&bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_private_account_dispatches_correctly() {
|
||||
let program_id: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let npk = NullifierPublicKey([3; 32]);
|
||||
let identifier: Identifier = 77;
|
||||
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(&npk, &PrivateAccountKind::Regular(identifier)),
|
||||
AccountId::for_regular_private_account(&npk, identifier),
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId::for_private_account(
|
||||
&npk,
|
||||
&PrivateAccountKind::Pda {
|
||||
program_id,
|
||||
seed,
|
||||
identifier
|
||||
}
|
||||
),
|
||||
AccountId::for_private_pda(&program_id, &seed, &npk, identifier),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_with_seeds() {
|
||||
let caller: ProgramId = [1; 8];
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(Some(caller), &[seed]);
|
||||
let expected = AccountId::for_public_pda(&caller, &seed);
|
||||
assert!(result.contains(&expected));
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
/// With no caller (top-level call), the result is always empty.
|
||||
#[test]
|
||||
fn compute_public_authorized_pdas_no_caller_returns_empty() {
|
||||
let seed = PdaSeed::new([2; 32]);
|
||||
let result = compute_public_authorized_pdas(None, &[seed]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
@ -164,398 +164,4 @@ const fn prev_power_of_two(x: usize) -> usize {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl MerkleTree {
|
||||
pub fn new(values: &[Value]) -> Self {
|
||||
let mut this = Self::with_capacity(values.len());
|
||||
for value in values.iter().copied() {
|
||||
this.insert(value);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_merkle_tree() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
let expected_root =
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_0() {
|
||||
let values = [[0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert_eq!(tree.root(), hash_value(&[0; 32]));
|
||||
assert_eq!(tree.capacity, 1);
|
||||
assert_eq!(tree.length, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_4() {
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499");
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 8);
|
||||
assert_eq!(tree.length, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_5() {
|
||||
let values = [
|
||||
[11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32],
|
||||
[13; 32], [15; 32], [11; 32],
|
||||
];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 16);
|
||||
assert_eq!(tree.length, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_6() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root =
|
||||
hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_4() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 7);
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}");
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}");
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_5() {
|
||||
let tree = MerkleTree::with_capacity(5);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 15);
|
||||
for i in 7..15 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]);
|
||||
}
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]);
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_6() {
|
||||
let mut tree = MerkleTree::with_capacity(100);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_7() {
|
||||
let mut tree = MerkleTree::with_capacity(599);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_8() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root =
|
||||
hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_1() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_2() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_3() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
tree.insert(values[0]);
|
||||
tree.insert(values[1]);
|
||||
tree.insert(values[2]);
|
||||
tree.insert(values[3]);
|
||||
tree.insert(values[4]);
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
// Reference implementation
|
||||
fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool {
|
||||
let mut result = hash_value(value);
|
||||
let mut level_index = index;
|
||||
for node in path {
|
||||
let is_left_child = level_index & 1 == 0;
|
||||
if is_left_child {
|
||||
result = hash_two(&result, node);
|
||||
} else {
|
||||
result = hash_two(node, &result);
|
||||
}
|
||||
level_index >>= 1;
|
||||
}
|
||||
&result == root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"),
|
||||
hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(2).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"),
|
||||
hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(0).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"),
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(4).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_4() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert!(tree.get_authentication_path_for(5).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_5() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let index = 4;
|
||||
let value = values[index];
|
||||
let path = tree.get_authentication_path_for(index).unwrap();
|
||||
assert!(verify_authentication_path(
|
||||
&value,
|
||||
index,
|
||||
&path,
|
||||
&tree.root()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_with_63_insertions() {
|
||||
let values = [
|
||||
hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"),
|
||||
hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"),
|
||||
hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"),
|
||||
hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"),
|
||||
hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"),
|
||||
hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"),
|
||||
hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"),
|
||||
hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"),
|
||||
hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"),
|
||||
hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"),
|
||||
hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"),
|
||||
hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"),
|
||||
hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"),
|
||||
hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"),
|
||||
hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"),
|
||||
hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"),
|
||||
hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"),
|
||||
hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"),
|
||||
hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"),
|
||||
hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"),
|
||||
hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"),
|
||||
hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"),
|
||||
hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"),
|
||||
hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"),
|
||||
hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"),
|
||||
hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"),
|
||||
hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"),
|
||||
hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"),
|
||||
hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"),
|
||||
hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"),
|
||||
hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"),
|
||||
hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"),
|
||||
hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"),
|
||||
hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"),
|
||||
hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"),
|
||||
hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"),
|
||||
hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"),
|
||||
hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"),
|
||||
hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"),
|
||||
hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"),
|
||||
hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"),
|
||||
hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"),
|
||||
hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"),
|
||||
hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"),
|
||||
hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"),
|
||||
hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"),
|
||||
hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"),
|
||||
hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"),
|
||||
hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"),
|
||||
hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"),
|
||||
hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"),
|
||||
hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"),
|
||||
hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"),
|
||||
hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"),
|
||||
hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"),
|
||||
hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"),
|
||||
hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"),
|
||||
hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"),
|
||||
hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"),
|
||||
hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"),
|
||||
hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"),
|
||||
hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"),
|
||||
hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"),
|
||||
];
|
||||
|
||||
let expected_root =
|
||||
hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0");
|
||||
|
||||
let mut tree_less_capacity = MerkleTree::with_capacity(1);
|
||||
let mut tree_exact_capacity = MerkleTree::with_capacity(64);
|
||||
let mut tree_more_capacity = MerkleTree::with_capacity(128);
|
||||
|
||||
for value in &values {
|
||||
tree_less_capacity.insert(*value);
|
||||
tree_exact_capacity.insert(*value);
|
||||
tree_more_capacity.insert(*value);
|
||||
}
|
||||
|
||||
assert_eq!(tree_more_capacity.root(), expected_root);
|
||||
assert_eq!(tree_less_capacity.root(), expected_root);
|
||||
assert_eq!(tree_exact_capacity.root(), expected_root);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
mod tests;
|
||||
|
||||
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
380
lee/state_machine/src/merkle_tree/tests.rs
Normal file
@ -0,0 +1,380 @@
|
||||
use hex_literal::hex;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl MerkleTree {
|
||||
pub fn new(values: &[Value]) -> Self {
|
||||
let mut this = Self::with_capacity(values.len());
|
||||
for value in values.iter().copied() {
|
||||
this.insert(value);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_merkle_tree() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
let expected_root = hex!("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_0() {
|
||||
let values = [[0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert_eq!(tree.root(), hash_value(&[0; 32]));
|
||||
assert_eq!(tree.capacity, 1);
|
||||
assert_eq!(tree.length, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [0; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("c9bbb83096df85157a146e7d770455a98412dee0633187ee86fee6c8a45b831a");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 4);
|
||||
assert_eq!(tree.length, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_4() {
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("ef418aed5aa20702d4d94c92da79a4012f2e36f1008bfdb3cd1e38749dca2499");
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 8);
|
||||
assert_eq!(tree.length, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_5() {
|
||||
let values = [
|
||||
[11; 32], [12; 32], [12; 32], [13; 32], [14; 32], [15; 32], [15; 32], [13; 32], [13; 32],
|
||||
[15; 32], [11; 32],
|
||||
];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("3f72d2ff55921a86c48e5988ec3e19ee9d0d5aa3e23197842970a903508ed767");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
assert_eq!(tree.capacity, 16);
|
||||
assert_eq!(tree.length, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merkle_tree_6() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_root = hex!("069cb8259a06fe6edb3fa7ff7933a6dd7dca6fca299314379794a688926c3792");
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_4() {
|
||||
let tree = MerkleTree::with_capacity(4);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 7);
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0], "{i}");
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1], "{i}");
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_5() {
|
||||
let tree = MerkleTree::with_capacity(5);
|
||||
|
||||
assert_eq!(tree.length, 0);
|
||||
assert_eq!(tree.nodes.len(), 15);
|
||||
for i in 7..15 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[0]);
|
||||
}
|
||||
for i in 3..7 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[1]);
|
||||
}
|
||||
for i in 1..3 {
|
||||
assert_eq!(*tree.get_node(i), default_values::DEFAULT_VALUES[2]);
|
||||
}
|
||||
assert_eq!(*tree.get_node(0), default_values::DEFAULT_VALUES[3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_6() {
|
||||
let mut tree = MerkleTree::with_capacity(100);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
|
||||
let expected_root = hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_7() {
|
||||
let mut tree = MerkleTree::with_capacity(599);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_capacity_8() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
|
||||
let expected_root = hex!("c8d3d8d2b13f27ceeccdc699119871f9f32ea7ed86ff45d0ad11f77b28cd7568");
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(tree.root(), expected_root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_1() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_2() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
assert_eq!(0, tree.insert(values[0]));
|
||||
assert_eq!(1, tree.insert(values[1]));
|
||||
assert_eq!(2, tree.insert(values[2]));
|
||||
assert_eq!(3, tree.insert(values[3]));
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_value_3() {
|
||||
let mut tree = MerkleTree::with_capacity(1);
|
||||
|
||||
let values = [[11; 32], [12; 32], [13; 32], [14; 32], [15; 32]];
|
||||
let expected_tree = MerkleTree::new(&values);
|
||||
|
||||
tree.insert(values[0]);
|
||||
tree.insert(values[1]);
|
||||
tree.insert(values[2]);
|
||||
tree.insert(values[3]);
|
||||
tree.insert(values[4]);
|
||||
|
||||
assert_eq!(expected_tree, tree);
|
||||
}
|
||||
|
||||
// Reference implementation
|
||||
fn verify_authentication_path(value: &Value, index: usize, path: &[Node], root: &Node) -> bool {
|
||||
let mut result = hash_value(value);
|
||||
let mut level_index = index;
|
||||
for node in path {
|
||||
let is_left_child = level_index & 1 == 0;
|
||||
if is_left_child {
|
||||
result = hash_two(&result, node);
|
||||
} else {
|
||||
result = hash_two(node, &result);
|
||||
}
|
||||
level_index >>= 1;
|
||||
}
|
||||
&result == root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_1() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("9f4fb68f3e1dac82202f9aa581ce0bbf1f765df0e9ac3c8c57e20f685abab8ed"),
|
||||
hex!("50a27d4746f357cb700cbe9d4883b77fb64f0128828a3489dc6a6f21ddbf2414"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(2).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_2() {
|
||||
let values = [[1; 32], [2; 32], [3; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("75877bb41d393b5fb8455ce60ecd8dda001d06316496b14dfa7f895656eeca4a"),
|
||||
hex!("a41b855d2db4de9052cd7be5ec67d6586629cb9f6e3246a4afa5ba313f07a9c5"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(0).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_3() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let expected_authentication_path = vec![
|
||||
hex!("0000000000000000000000000000000000000000000000000000000000000000"),
|
||||
hex!("f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b"),
|
||||
hex!("48c73f7821a58a8d2a703e5b39c571c0aa20cf14abcd0af8f2b955bc202998de"),
|
||||
];
|
||||
|
||||
let authentication_path = tree.get_authentication_path_for(4).unwrap();
|
||||
assert_eq!(authentication_path, expected_authentication_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_4() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
assert!(tree.get_authentication_path_for(5).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authentication_path_5() {
|
||||
let values = [[1; 32], [2; 32], [3; 32], [4; 32], [5; 32]];
|
||||
let tree = MerkleTree::new(&values);
|
||||
let index = 4;
|
||||
let value = values[index];
|
||||
let path = tree.get_authentication_path_for(index).unwrap();
|
||||
assert!(verify_authentication_path(
|
||||
&value,
|
||||
index,
|
||||
&path,
|
||||
&tree.root()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_with_63_insertions() {
|
||||
let values = [
|
||||
hex!("cd00acab0f45736e6c6311f1953becc0b69a062e7c2a7310875d28bdf9ef9c5b"),
|
||||
hex!("0df5a6afbcc7bf126caf7084acfc593593ab512e6ca433c61c1a922be40a04ea"),
|
||||
hex!("23c1258620266c7bedb6d1ee32f6da9413e4010ace975239dccb34e727e07c40"),
|
||||
hex!("f33ccc3a11476b0ef62326ca5ec292056759b05e6a28023d2d1ce66165611353"),
|
||||
hex!("77f914ab016b8049f6bea7704000e413a393865918a3824f9285c3db0aacff23"),
|
||||
hex!("910a1c23188e54d57fd167ddb0f8bf68c6b70ed9ec76ef56c4b7f2632f82ca7f"),
|
||||
hex!("047ee85526197d1e7403a559cf6d2f22c1926c8ad59481a2e2f1b697af45e40b"),
|
||||
hex!("9d355cf89fb382ae34bf80566b28489278d10f2cebb5b0ea42fab1bac5adae0c"),
|
||||
hex!("604018b95232596b2685a9bc737b6cccb53b10e483d2d9a2f4a755410b02a188"),
|
||||
hex!("a16708ef7b6bf1796063addaf57d6a566b6f87b0bbe42af43a4590d05f1684cb"),
|
||||
hex!("820f2dfa271cd2fd41e1452406d5dad552c85c1223c45d45dbd7446759fdc6b8"),
|
||||
hex!("680b6912d7e219f8805d4d28adb4428dd78fea0dc1b8cdb2412645c4b1962c88"),
|
||||
hex!("14d5471ce6c45506753982b17cac5790ac7bc29e6f388f31052d7dfd62b294e5"),
|
||||
hex!("8b364200172b777d4aa16d2098b5eb98ac3dd4a1b9597e5c2bf6f6930031f230"),
|
||||
hex!("9bb45b910711874339dda8a21a9aad73822286f5e52d7d3de0ed78dfbba329a5"),
|
||||
hex!("d6806d5df5cb25ce5d531042f09b3cb34fb9e47c61182b63cccd9d44392f6027"),
|
||||
hex!("b8cfa90ebc8fd09c04682d93a08fddd3e8e57715174dcc92451edd191264a58b"),
|
||||
hex!("3463c7f81d00f809b3dfa83195447c927fb4045b3913dac6f45bee6c4010d7ed"),
|
||||
hex!("1d6ad7f7d677905feb506c58f4b404a79370ebc567296abea3a368b61d5a8239"),
|
||||
hex!("a58085ecf00963cb22da23c901b9b3ddc56462bb96ff03c923d67708e10dd29c"),
|
||||
hex!("c3319f4a65fb5bbb8447137b0972c03cbd84ebf7d9da194e0fcbd68c2d4d5bdb"),
|
||||
hex!("4aa31e90e0090faf3648d05e5d5499df2c78ebed4d6e6c23d8147de5d67dae73"),
|
||||
hex!("9f33b1d2c8bc7bd265336de1033ede6344bc41260313bdcb43f1108b83b9be92"),
|
||||
hex!("6500d4ad93d41c16ec81eaa5e70f173194aabe5c1072ac263b5727296f5b7cac"),
|
||||
hex!("3584f5d260003669fad98786e13171376b0f19410cb232ce65606cbff79e6768"),
|
||||
hex!("c8410946ebf56f13141c894a34ced85a5230088af70dcea581e44f52847830ac"),
|
||||
hex!("71dd90281cdebb70422f2d04ae446d5d2d5ea64b803c16128d37e3fcd5d1a4cc"),
|
||||
hex!("c05acf8d77ab4d659a538bd35af590864a7ad9c055ff5d6cda9d5aecfccecba3"),
|
||||
hex!("f1df98822ea084cce9021aa9cc81b1746cd1e84a75690da63e10fd877633ed77"),
|
||||
hex!("2ca822bc8f67bceb0a71a0d06fea7349036ef3e5ec21795a851e4182bd35ce01"),
|
||||
hex!("7fd2179abc3bcf89b4d8092988ba8c23952b3bbd3d7caea6b5ea0c13cf19f68b"),
|
||||
hex!("91b6ad516e017f6aa5a2e95776538bd3a3e933c1b1d32bb5e0f00a9db63c9c24"),
|
||||
hex!("cd31a8b5eef5ca0be5ef1cb261d0bf0a74d774a3152bb99739cfd296a1d0b85e"),
|
||||
hex!("3fb16f48b2bf93f3815979e6638f975d7f935088ec37db0be0f07965fbc78339"),
|
||||
hex!("c60c61b99bf486af5f4bf780a69860dafcd35c1474306a8575666fb5449bcec0"),
|
||||
hex!("8048d0d7e14091251f3f6c6b10bf6b5880a014b513f9f8c2395501dbffa6192a"),
|
||||
hex!("778b5af10b9dbe80b60a8e4f0bb91caf4476bcb812801099760754ae623fbd84"),
|
||||
hex!("d3ac25467920a4e08998b7a3226b8b54bfe66ac58cfedc71f15b2402fee0054a"),
|
||||
hex!("029aa94598fae2961a0d43937b8a9a3138bcfeae99a7cb15f77fac7c506f8432"),
|
||||
hex!("2eee5ef52fe669cb6882a68c893abdc1262dcf4424e4ba7a479da7cf1c10171d"),
|
||||
hex!("de3fb3d070e3a90f0eed8b5e65088a8dc0e4e3c342b9c0bf33bab714eae5dfec"),
|
||||
hex!("14d40177e833ab45bbfdc5f2b11fba7efaebb3f69facc554f24b549a2efe8538"),
|
||||
hex!("5734355069702448774fb2df95f1d562e1b9fe1514aeb6b922554ee9d2d01068"),
|
||||
hex!("8a273d49ac110343cec2cf3359d16eb2906b446bd9ec9833e2a640cebc8d5155"),
|
||||
hex!("e3fa984dd3cbeb9a7e827ed32d3d4e6a6ba643a55d82be97d9ddb06ee809fa3e"),
|
||||
hex!("90b1d5a364e17c8b7965396b06ec6e13749b5fc16500731518ad8fc30ae33e77"),
|
||||
hex!("7517376541b2e8ec83cbab04522b54a26610908a9872feb663451385aea58eb1"),
|
||||
hex!("5cba2e4cf7448e526d161133c4b2ea7c919ac4813a7308612595f46f11dea6cd"),
|
||||
hex!("c721911b300bec0691c8a2dfaabfef1d66b7b6258918914d3c3ad690729f05b7"),
|
||||
hex!("d0d0a70d8ae0d27806fa0b711c507290c260a89cbca0436d339d1dccdd087d62"),
|
||||
hex!("2a625c28ea763c5e82dd0a93ecfca7ec371ccbb363cd42be359c2c875f58009d"),
|
||||
hex!("174ef0119932ed890397d9f3837dd85f9100558b6fc9085d4af947ae8cf74bbc"),
|
||||
hex!("b497bc267151e8efa3c6daa461e6804b01a3f05f44f1f4d5b41d5f0d3f5219b1"),
|
||||
hex!("e987e91f5734630ddd7e6b58733b4fcdbc316ee9e8cac0e94c36c91cf58e59cc"),
|
||||
hex!("55019ad8bbe656c51eb042190c1c8da53f42baf43fd2350ebea38fc7cca2fae3"),
|
||||
hex!("c45a638edd18a6d9f5ad20b870c81b8626459bcb22dae7d58add7a6b6c6a84a8"),
|
||||
hex!("d42d3a5fb2ad50b2027fe5a36d59dd71e49a63e4b1b299073c96bbf7ba5d68a1"),
|
||||
hex!("9599e561054bcd3f647eb018ab0b069d3176497d42be9c4466551cbb959be47c"),
|
||||
hex!("42f33b23775327ff71aea6569548255f3cc9929da73373cc9bb1743d417f7cda"),
|
||||
hex!("ab24294f44fc6fdbeb96e0f6e93c4f6d97d035b73b9a337c353e18c6d0603bdd"),
|
||||
hex!("33954ec63520334f99b640a2982ac966b68c363fed383d621a1ab573934f1d33"),
|
||||
hex!("5e2a1f7df963d1fd8f50a285387cfbb5df581426619b325563e20bf7886c62b7"),
|
||||
hex!("13ffde471d4e27c473254e766fd1328ad80c42cab4d4955cffeae43d866f86e5"),
|
||||
];
|
||||
|
||||
let expected_root = hex!("1cf9b214217d7823f9de51b8f6cb34d0a99436a3a1bb762f90b815672a6afcc0");
|
||||
|
||||
let mut tree_less_capacity = MerkleTree::with_capacity(1);
|
||||
let mut tree_exact_capacity = MerkleTree::with_capacity(64);
|
||||
let mut tree_more_capacity = MerkleTree::with_capacity(128);
|
||||
|
||||
for value in &values {
|
||||
tree_less_capacity.insert(*value);
|
||||
tree_exact_capacity.insert(*value);
|
||||
tree_more_capacity.insert(*value);
|
||||
}
|
||||
|
||||
assert_eq!(tree_more_capacity.root(), expected_root);
|
||||
assert_eq!(tree_less_capacity.root(), expected_root);
|
||||
assert_eq!(tree_exact_capacity.root(), expected_root);
|
||||
}
|
||||
@ -1,916 +0,0 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
account::AccountWithMetadata,
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
/// Proof of the privacy preserving execution circuit.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Proof(pub(crate) Vec<u8>);
|
||||
|
||||
impl Proof {
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_inner(inner: Vec<u8>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool {
|
||||
let Ok(inner) = borsh::from_slice::<InnerReceipt>(&self.0) else {
|
||||
return false;
|
||||
};
|
||||
let receipt = Receipt::new(inner, circuit_output.to_bytes());
|
||||
receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
pub dependencies: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl ProgramWithDependencies {
|
||||
#[must_use]
|
||||
pub const fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
|
||||
Self {
|
||||
program,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Program> for ProgramWithDependencies {
|
||||
fn from(program: Program) -> Self {
|
||||
Self::new(program, HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a proof of the execution of a LEE program inside the privacy preserving execution
|
||||
/// circuit.
|
||||
pub fn execute_and_prove(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_data: InstructionData,
|
||||
account_identities: Vec<InputAccountIdentity>,
|
||||
program_with_dependencies: &ProgramWithDependencies,
|
||||
) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> {
|
||||
let ProgramWithDependencies {
|
||||
program: initial_program,
|
||||
dependencies,
|
||||
} = program_with_dependencies;
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
let mut program_outputs = Vec::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: initial_program.id(),
|
||||
instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(LeeError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
|
||||
let program_output: ProgramOutput = inner_receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
// TODO: remove clone
|
||||
program_outputs.push(program_output.clone());
|
||||
|
||||
// Prove circuit.
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
let circuit_input = PrivacyPreservingCircuitInput {
|
||||
program_outputs,
|
||||
account_identities,
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
let env = env_builder.build().unwrap();
|
||||
let prover = default_prover();
|
||||
let opts = ProverOpts::succinct();
|
||||
let prove_info = prover
|
||||
.prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts)
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?);
|
||||
|
||||
let circuit_output: PrivacyPreservingCircuitOutput = prove_info
|
||||
.receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
Ok((circuit_output, proof))
|
||||
}
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<Receipt, LeeError> {
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
Program::write_inputs(
|
||||
program.id(),
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
&mut env_builder,
|
||||
)?;
|
||||
let env = env_builder.build().unwrap();
|
||||
|
||||
// Prove the program
|
||||
let prover = default_prover();
|
||||
Ok(prover
|
||||
.prove(env, program.elf())
|
||||
.map_err(|e| LeeError::ProgramProveFailed(e.to_string()))?
|
||||
.receipt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme,
|
||||
EphemeralPublicKey, Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
privacy_preserving_transaction::circuit::execute_and_prove,
|
||||
program::Program,
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
fn decrypt_kind(
|
||||
output: &PrivacyPreservingCircuitOutput,
|
||||
ssk: &SharedSecretKey,
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
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();
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let expected_sender_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: Nonce::default(),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_sender_pre = sender.clone();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
let [sender_post] = output.public_post_states.try_into().unwrap();
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
program_owner: program.id(),
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), 0),
|
||||
);
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let mut commitment_set = CommitmentSet::with_capacity(2);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment_sender));
|
||||
let expected_new_nullifiers = vec![
|
||||
(
|
||||
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
|
||||
commitment_set.digest(),
|
||||
),
|
||||
(
|
||||
Nullifier::for_account_initialization(&recipient_account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
),
|
||||
];
|
||||
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let expected_private_account_1 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_private_account_2 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_new_commitments = vec![
|
||||
Commitment::new(&sender_account_id, &expected_private_account_1),
|
||||
Commitment::new(&recipient_account_id, &expected_private_account_2),
|
||||
];
|
||||
|
||||
let shared_secret_1 =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let shared_secret_2 =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient],
|
||||
Program::serialize_instruction(balance_to_move).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
|
||||
.get_proof_for(&commitment_sender)
|
||||
.expect("sender's commitment must be in the set"),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
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.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&account_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller();
|
||||
let validity_window = crate::test_methods::validity_window();
|
||||
|
||||
let instruction = Program::serialize_instruction((
|
||||
Some(1_u64),
|
||||
Some(4_u64),
|
||||
validity_window.id(),
|
||||
Some(4_u64),
|
||||
Some(7_u64),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
validity_window_chain_caller,
|
||||
[(validity_window.id(), validity_window)].into(),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let (output, _proof) = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &shared_secret, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// PDA init: initializes a new PDA under `simple_balance_transfer`'s ownership.
|
||||
/// The `simple_transfer_proxy` program chains to `simple_balance_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the private PDA binding.
|
||||
#[test]
|
||||
fn private_pda_init() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Recipient (public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses the standard unauthorized private account path and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_simple_transfer() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
// Recipient: shared private account (new, unauthorized)
|
||||
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(
|
||||
&shared_npk,
|
||||
&shared_keys.vpk(),
|
||||
),
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![recipient],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_update_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let account = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment));
|
||||
|
||||
let sender = AccountWithMetadata::new(account, true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender],
|
||||
Program::serialize_instruction(()).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(),
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program.clone(),
|
||||
[(simple_transfer_id, simple_transfer)].into(),
|
||||
);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_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(),
|
||||
identifier,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_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(),
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
InputAccountIdentity, PrivacyPreservingCircuitInput, PrivacyPreservingCircuitOutput,
|
||||
account::AccountWithMetadata,
|
||||
program::{ChainedCall, InstructionData, ProgramId, ProgramOutput},
|
||||
};
|
||||
use risc0_zkvm::{ExecutorEnv, InnerReceipt, ProverOpts, Receipt, default_prover};
|
||||
|
||||
use crate::{
|
||||
PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
/// Proof of the privacy preserving execution circuit.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
|
||||
pub struct Proof(pub(crate) Vec<u8>);
|
||||
|
||||
impl Proof {
|
||||
#[must_use]
|
||||
pub fn into_inner(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn from_inner(inner: Vec<u8>) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid_for(&self, circuit_output: &PrivacyPreservingCircuitOutput) -> bool {
|
||||
let Ok(inner) = borsh::from_slice::<InnerReceipt>(&self.0) else {
|
||||
return false;
|
||||
};
|
||||
let receipt = Receipt::new(inner, circuit_output.to_bytes());
|
||||
receipt.verify(PRIVACY_PRESERVING_CIRCUIT_ID).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProgramWithDependencies {
|
||||
pub program: Program,
|
||||
// TODO: avoid having a copy of the bytecode of each dependency.
|
||||
pub dependencies: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl ProgramWithDependencies {
|
||||
#[must_use]
|
||||
pub const fn new(program: Program, dependencies: HashMap<ProgramId, Program>) -> Self {
|
||||
Self {
|
||||
program,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Program> for ProgramWithDependencies {
|
||||
fn from(program: Program) -> Self {
|
||||
Self::new(program, HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a proof of the execution of a LEE program inside the privacy preserving execution
|
||||
/// circuit.
|
||||
pub fn execute_and_prove(
|
||||
pre_states: Vec<AccountWithMetadata>,
|
||||
instruction_data: InstructionData,
|
||||
account_identities: Vec<InputAccountIdentity>,
|
||||
program_with_dependencies: &ProgramWithDependencies,
|
||||
) -> Result<(PrivacyPreservingCircuitOutput, Proof), LeeError> {
|
||||
let ProgramWithDependencies {
|
||||
program: initial_program,
|
||||
dependencies,
|
||||
} = program_with_dependencies;
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
let mut program_outputs = Vec::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: initial_program.id(),
|
||||
instruction_data,
|
||||
pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let mut chained_calls = VecDeque::from_iter([(initial_call, initial_program, None)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
while let Some((chained_call, program, caller_program_id)) = chained_calls.pop_front() {
|
||||
if chain_calls_counter >= MAX_NUMBER_CHAINED_CALLS {
|
||||
return Err(LeeError::MaxChainedCallsDepthExceeded);
|
||||
}
|
||||
|
||||
let inner_receipt = execute_and_prove_program(
|
||||
program,
|
||||
caller_program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
|
||||
let program_output: ProgramOutput = inner_receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::ProgramOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
// TODO: remove clone
|
||||
program_outputs.push(program_output.clone());
|
||||
|
||||
// Prove circuit.
|
||||
env_builder.add_assumption(inner_receipt);
|
||||
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
let next_program = dependencies.get(&new_call.program_id).ok_or(
|
||||
InvalidProgramBehaviorError::UndeclaredProgramDependency {
|
||||
program_id: new_call.program_id,
|
||||
},
|
||||
)?;
|
||||
chained_calls.push_front((new_call, next_program, Some(chained_call.program_id)));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
let circuit_input = PrivacyPreservingCircuitInput {
|
||||
program_outputs,
|
||||
account_identities,
|
||||
program_id: program_with_dependencies.program.id(),
|
||||
};
|
||||
|
||||
env_builder.write(&circuit_input).unwrap();
|
||||
let env = env_builder.build().unwrap();
|
||||
let prover = default_prover();
|
||||
let opts = ProverOpts::succinct();
|
||||
let prove_info = prover
|
||||
.prove_with_opts(env, PRIVACY_PRESERVING_CIRCUIT_ELF, &opts)
|
||||
.map_err(|e| LeeError::CircuitProvingError(e.to_string()))?;
|
||||
|
||||
let proof = Proof(borsh::to_vec(&prove_info.receipt.inner)?);
|
||||
|
||||
let circuit_output: PrivacyPreservingCircuitOutput = prove_info
|
||||
.receipt
|
||||
.journal
|
||||
.decode()
|
||||
.map_err(|e| LeeError::CircuitOutputDeserializationError(e.to_string()))?;
|
||||
|
||||
Ok((circuit_output, proof))
|
||||
}
|
||||
|
||||
fn execute_and_prove_program(
|
||||
program: &Program,
|
||||
caller_program_id: Option<ProgramId>,
|
||||
pre_states: &[AccountWithMetadata],
|
||||
instruction_data: &InstructionData,
|
||||
) -> Result<Receipt, LeeError> {
|
||||
// Write inputs to the program
|
||||
let mut env_builder = ExecutorEnv::builder();
|
||||
Program::write_inputs(
|
||||
program.id(),
|
||||
caller_program_id,
|
||||
pre_states,
|
||||
instruction_data,
|
||||
&mut env_builder,
|
||||
)?;
|
||||
let env = env_builder.build().unwrap();
|
||||
|
||||
// Prove the program
|
||||
let prover = default_prover();
|
||||
Ok(prover
|
||||
.prove(env, program.elf())
|
||||
.map_err(|e| LeeError::ProgramProveFailed(e.to_string()))?
|
||||
.receipt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -0,0 +1,731 @@
|
||||
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
|
||||
|
||||
use lee_core::{
|
||||
Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, EncryptionScheme, EphemeralPublicKey,
|
||||
Nullifier, PrivacyPreservingCircuitOutput, SharedSecretKey,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
program::{PdaSeed, PrivateAccountKind},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
privacy_preserving_transaction::circuit::execute_and_prove,
|
||||
program::Program,
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
fn decrypt_kind(
|
||||
output: &PrivacyPreservingCircuitOutput,
|
||||
ssk: &SharedSecretKey,
|
||||
idx: usize,
|
||||
) -> PrivateAccountKind {
|
||||
let (kind, _) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[idx].ciphertext,
|
||||
ssk,
|
||||
&output.new_commitments[idx],
|
||||
u32::try_from(idx).expect("idx fits in u32"),
|
||||
)
|
||||
.unwrap();
|
||||
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();
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let expected_sender_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: Nonce::default(),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
data: Data::default(),
|
||||
};
|
||||
|
||||
let expected_sender_pre = sender.clone();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
|
||||
let [sender_pre] = output.public_pre_states.try_into().unwrap();
|
||||
let [sender_post] = output.public_post_states.try_into().unwrap();
|
||||
assert_eq!(sender_pre, expected_sender_pre);
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
assert_eq!(output.new_nullifiers.len(), 1);
|
||||
assert_eq!(output.encrypted_private_post_states.len(), 1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret,
|
||||
&output.new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prove_privacy_preserving_execution_circuit_fully_private() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
program_owner: program.id(),
|
||||
data: Data::default(),
|
||||
},
|
||||
true,
|
||||
AccountId::for_regular_private_account(&sender_keys.npk(), 0),
|
||||
);
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let commitment_sender = Commitment::new(&sender_account_id, &sender_pre.account);
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_account_id);
|
||||
let balance_to_move: u128 = 37;
|
||||
|
||||
let mut commitment_set = CommitmentSet::with_capacity(2);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment_sender));
|
||||
let expected_new_nullifiers = vec![
|
||||
(
|
||||
Nullifier::for_account_update(&commitment_sender, &sender_keys.nsk),
|
||||
commitment_set.digest(),
|
||||
),
|
||||
(
|
||||
Nullifier::for_account_initialization(&recipient_account_id),
|
||||
DUMMY_COMMITMENT_HASH,
|
||||
),
|
||||
];
|
||||
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let expected_private_account_1 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100 - balance_to_move,
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_private_account_2 = Account {
|
||||
program_owner: program.id(),
|
||||
balance: balance_to_move,
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
..Default::default()
|
||||
};
|
||||
let expected_new_commitments = vec![
|
||||
Commitment::new(&sender_account_id, &expected_private_account_1),
|
||||
Commitment::new(&recipient_account_id, &expected_private_account_2),
|
||||
];
|
||||
|
||||
let shared_secret_1 =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let shared_secret_2 =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1).0;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient],
|
||||
Program::serialize_instruction(balance_to_move).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
|
||||
.get_proof_for(&commitment_sender)
|
||||
.expect("sender's commitment must be in the set"),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(proof.is_valid_for(&output));
|
||||
assert!(output.public_pre_states.is_empty());
|
||||
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.encrypted_private_post_states.len(), 2);
|
||||
|
||||
let (_identifier, sender_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[0].ciphertext,
|
||||
&shared_secret_1,
|
||||
&expected_new_commitments[0],
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sender_post, expected_private_account_1);
|
||||
|
||||
let (_identifier, recipient_post) = EncryptionScheme::decrypt(
|
||||
&output.encrypted_private_post_states[1].ciphertext,
|
||||
&shared_secret_2,
|
||||
&expected_new_commitments[1],
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(recipient_post, expected_private_account_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circuit_fails_when_chained_validity_windows_have_empty_intersection() {
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(
|
||||
Account::default(),
|
||||
false,
|
||||
AccountId::for_regular_private_account(&account_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let validity_window_chain_caller = crate::test_methods::validity_window_chain_caller();
|
||||
let validity_window = crate::test_methods::validity_window();
|
||||
|
||||
let instruction = Program::serialize_instruction((
|
||||
Some(1_u64),
|
||||
Some(4_u64),
|
||||
validity_window.id(),
|
||||
Some(4_u64),
|
||||
Some(7_u64),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
validity_window_chain_caller,
|
||||
[(validity_window.id(), validity_window)].into(),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
/// A private PDA claimed with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_claim_with_custom_identifier_encrypts_correct_kind() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let (output, _proof) = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.clone().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &shared_secret, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// PDA init: initializes a new PDA under `simple_balance_transfer`'s ownership.
|
||||
/// The `simple_transfer_proxy` program chains to `simple_balance_transfer` with `pda_seeds`
|
||||
/// to establish authorization and the private PDA binding.
|
||||
#[test]
|
||||
fn private_pda_init() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=false triggers init path (1 pre-state)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, false)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre],
|
||||
instruction,
|
||||
vec![InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA init should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// PDA withdraw: chains to `simple_balance_transfer` to move balance from PDA to recipient.
|
||||
/// Uses a default PDA (amount=0) because testing with a pre-funded PDA requires a
|
||||
/// two-tx sequence with membership proofs.
|
||||
#[test]
|
||||
fn private_pda_withdraw() {
|
||||
let program = crate::test_methods::simple_transfer_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret_pda =
|
||||
SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// PDA (new, private PDA)
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 0);
|
||||
let pda_pre = AccountWithMetadata::new(Account::default(), false, pda_id);
|
||||
|
||||
// Recipient (public)
|
||||
let recipient_id = AccountId::new([88; 32]);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: 10000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
recipient_id,
|
||||
);
|
||||
|
||||
let auth_id = simple_transfer.id();
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(auth_id, simple_transfer)].into());
|
||||
|
||||
// is_withdraw=true, amount=0 (PDA has no balance yet)
|
||||
let instruction = Program::serialize_instruction((seed, auth_id, 0_u128, true)).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::PrivatePdaInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&npk, &keys.vpk()),
|
||||
npk,
|
||||
ssk: shared_secret_pda,
|
||||
identifier: 0,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("PDA withdraw should succeed");
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// Shared regular private account: receives funds via `authenticated_transfer` directly,
|
||||
/// no custom program needed. This demonstrates the non-PDA shared account flow where
|
||||
/// keys are derived from GMS via `derive_keys_for_shared_account`. The shared account
|
||||
/// uses the standard unauthorized private account path and works with auth-transfer's
|
||||
/// transfer path like any other private account.
|
||||
#[test]
|
||||
fn shared_account_receives_via_simple_transfer() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let shared_keys = test_private_account_keys_1();
|
||||
let shared_npk = shared_keys.npk();
|
||||
let shared_identifier: u128 = 42;
|
||||
let shared_secret =
|
||||
SharedSecretKey::encapsulate_deterministic(&shared_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
// Sender: public account with balance, owned by auth-transfer
|
||||
let sender_id = AccountId::new([99; 32]);
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
sender_id,
|
||||
);
|
||||
|
||||
// Recipient: shared private account (new, unauthorized)
|
||||
let shared_account_id = AccountId::from((&shared_npk, shared_identifier));
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, shared_account_id);
|
||||
|
||||
let balance_to_move: u128 = 100;
|
||||
let instruction = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
instruction,
|
||||
vec![
|
||||
InputAccountIdentity::Public,
|
||||
InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&shared_npk, &shared_keys.vpk()),
|
||||
npk: shared_npk,
|
||||
ssk: shared_secret,
|
||||
identifier: shared_identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
let (output, _proof) = result.expect("shared account receive should succeed");
|
||||
// Sender is public (no commitment), recipient is private (1 commitment)
|
||||
assert_eq!(output.new_commitments.len(), 1);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedInit` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let pre = AccountWithMetadata::new(Account::default(), true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pre],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateAuthorizedInit {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
ssk,
|
||||
nsk: keys.nsk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateUnauthorized` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_unauthorized_init_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let recipient_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, recipient_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![recipient],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::PrivateUnauthorized {
|
||||
epk: EphemeralPublicKey(Vec::new()),
|
||||
view_tag: EncryptedAccountData::compute_view_tag(&keys.npk(), &keys.vpk()),
|
||||
npk: keys.npk(),
|
||||
ssk,
|
||||
identifier,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivateAuthorizedUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Regular` carrying the correct identifier.
|
||||
#[test]
|
||||
fn private_authorized_update_encrypts_regular_kind_with_identifier() {
|
||||
let program = crate::test_methods::noop();
|
||||
let keys = test_private_account_keys_1();
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), identifier);
|
||||
let account = Account {
|
||||
program_owner: program.id(),
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let commitment = Commitment::new(&account_id, &account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&commitment));
|
||||
|
||||
let sender = AccountWithMetadata::new(account, true, account_id);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![sender],
|
||||
Program::serialize_instruction(()).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(),
|
||||
identifier,
|
||||
}],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Regular(identifier)
|
||||
);
|
||||
}
|
||||
|
||||
/// `PrivatePdaUpdate` with a non-default identifier produces a ciphertext that decrypts
|
||||
/// to `PrivateAccountKind::Pda` carrying the correct `(program_id, seed, identifier)`.
|
||||
#[test]
|
||||
fn private_pda_update_encrypts_pda_kind_with_identifier() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let identifier: u128 = 99;
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, identifier);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
program.clone(),
|
||||
[(simple_transfer_id, simple_transfer)].into(),
|
||||
);
|
||||
|
||||
let (output, _) = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_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(),
|
||||
identifier,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decrypt_kind(&output, &ssk, 0),
|
||||
PrivateAccountKind::Pda {
|
||||
program_id: program.id(),
|
||||
seed,
|
||||
identifier
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_init_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_claimer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let shared_secret = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let account_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pre_state = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
seed: None,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_pda_update_identifier_mismatch_fails() {
|
||||
let program = crate::test_methods::pda_spend_proxy();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let keys = test_private_account_keys_1();
|
||||
let npk = keys.npk();
|
||||
let seed = PdaSeed::new([42; 32]);
|
||||
let ssk = SharedSecretKey::encapsulate_deterministic(&keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let simple_transfer_id = simple_transfer.id();
|
||||
let pda_id = AccountId::for_private_pda(&program.id(), &seed, &npk, 5);
|
||||
let pda_account = Account {
|
||||
program_owner: simple_transfer_id,
|
||||
balance: 1,
|
||||
..Account::default()
|
||||
};
|
||||
let pda_commitment = Commitment::new(&pda_id, &pda_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&pda_commitment));
|
||||
|
||||
let pda_pre = AccountWithMetadata::new(pda_account, true, pda_id);
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, AccountId::new([0; 32]));
|
||||
|
||||
let program_with_deps =
|
||||
ProgramWithDependencies::new(program, [(simple_transfer_id, simple_transfer)].into());
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![pda_pre, recipient_pre],
|
||||
Program::serialize_instruction((seed, 1_u128, simple_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(),
|
||||
identifier: 99,
|
||||
seed: None,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
@ -111,42 +111,4 @@ impl Program {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lee_core::account::{Account, AccountId, AccountWithMetadata};
|
||||
|
||||
use crate::program::Program;
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let balance_to_move: u128 = 11_223_344_556_677;
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 77_665_544_332_211,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let recipient =
|
||||
AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
|
||||
|
||||
let expected_sender_post = Account {
|
||||
balance: 77_665_544_332_211 - balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let expected_recipient_post = Account {
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.unwrap();
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
assert_eq!(sender_post.account(), &expected_sender_post);
|
||||
assert_eq!(recipient_post.account(), &expected_recipient_post);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
36
lee/state_machine/src/program/tests.rs
Normal file
36
lee/state_machine/src/program/tests.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use lee_core::account::{Account, AccountId, AccountWithMetadata};
|
||||
|
||||
use crate::program::Program;
|
||||
|
||||
#[test]
|
||||
fn program_execution() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let balance_to_move: u128 = 11_223_344_556_677;
|
||||
let instruction_data = Program::serialize_instruction(balance_to_move).unwrap();
|
||||
let sender = AccountWithMetadata::new(
|
||||
Account {
|
||||
balance: 77_665_544_332_211,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, AccountId::new([1; 32]));
|
||||
|
||||
let expected_sender_post = Account {
|
||||
balance: 77_665_544_332_211 - balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let expected_recipient_post = Account {
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
};
|
||||
let program_output = program
|
||||
.execute(None, &[sender, recipient], &instruction_data)
|
||||
.unwrap();
|
||||
|
||||
let [sender_post, recipient_post] = program_output.post_states.try_into().unwrap();
|
||||
|
||||
assert_eq!(sender_post.account(), &expected_sender_post);
|
||||
assert_eq!(recipient_post.account(), &expected_recipient_post);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
320
lee/state_machine/src/state/mod.rs
Normal file
320
lee/state_machine/src/state/mod.rs
Normal file
@ -0,0 +1,320 @@
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
use borsh::{BorshDeserialize, BorshSerialize};
|
||||
use lee_core::{
|
||||
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
|
||||
Timestamp,
|
||||
account::{Account, AccountId},
|
||||
program::ProgramId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::LeeError,
|
||||
merkle_tree::MerkleTree,
|
||||
privacy_preserving_transaction::PrivacyPreservingTransaction,
|
||||
program::Program,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
validated_state_diff::{StateDiff, ValidatedStateDiff},
|
||||
};
|
||||
|
||||
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
|
||||
|
||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
pub struct CommitmentSet {
|
||||
merkle_tree: MerkleTree,
|
||||
commitments: HashMap<Commitment, usize>,
|
||||
root_history: HashSet<CommitmentSetDigest>,
|
||||
}
|
||||
|
||||
impl CommitmentSet {
|
||||
pub(crate) fn digest(&self) -> CommitmentSetDigest {
|
||||
self.merkle_tree.root()
|
||||
}
|
||||
|
||||
/// Queries the `CommitmentSet` for a membership proof of commitment.
|
||||
pub fn get_proof_for(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
let index = *self.commitments.get(commitment)?;
|
||||
|
||||
self.merkle_tree
|
||||
.get_authentication_path_for(index)
|
||||
.map(|path| (index, path))
|
||||
}
|
||||
|
||||
/// Inserts a list of commitments to the `CommitmentSet`.
|
||||
pub(crate) fn extend(&mut self, commitments: &[Commitment]) {
|
||||
for commitment in commitments.iter().cloned() {
|
||||
let index = self.merkle_tree.insert(commitment.to_byte_array());
|
||||
self.commitments.insert(commitment, index);
|
||||
}
|
||||
self.root_history.insert(self.digest());
|
||||
}
|
||||
|
||||
fn contains(&self, commitment: &Commitment) -> bool {
|
||||
self.commitments.contains_key(commitment)
|
||||
}
|
||||
|
||||
/// Initializes an empty `CommitmentSet` with a given capacity.
|
||||
/// If the capacity is not a `power_of_two`, then capacity is taken
|
||||
/// to be the next `power_of_two`.
|
||||
pub(crate) fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
merkle_tree: MerkleTree::with_capacity(capacity),
|
||||
commitments: HashMap::new(),
|
||||
root_history: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
#[derive(Clone)]
|
||||
struct NullifierSet(BTreeSet<Nullifier>);
|
||||
|
||||
impl NullifierSet {
|
||||
const fn new() -> Self {
|
||||
Self(BTreeSet::new())
|
||||
}
|
||||
|
||||
fn extend(&mut self, new_nullifiers: &[Nullifier]) {
|
||||
self.0.extend(new_nullifiers);
|
||||
}
|
||||
|
||||
fn contains(&self, nullifier: &Nullifier) -> bool {
|
||||
self.0.contains(nullifier)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorshSerialize for NullifierSet {
|
||||
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
|
||||
self.0.iter().collect::<Vec<_>>().serialize(writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorshDeserialize for NullifierSet {
|
||||
fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
|
||||
let vec = Vec::<Nullifier>::deserialize_reader(reader)?;
|
||||
|
||||
let mut set = BTreeSet::new();
|
||||
for n in vec {
|
||||
if !set.insert(n) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"duplicate nullifier in NullifierSet",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(set))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, BorshSerialize, BorshDeserialize)]
|
||||
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
|
||||
pub struct V03State {
|
||||
public_state: HashMap<AccountId, Account>,
|
||||
private_state: (CommitmentSet, NullifierSet),
|
||||
programs: HashMap<ProgramId, Program>,
|
||||
}
|
||||
|
||||
impl Default for V03State {
|
||||
fn default() -> Self {
|
||||
let mut commitment_set = CommitmentSet::with_capacity(32);
|
||||
commitment_set.extend(&[DUMMY_COMMITMENT]);
|
||||
let nullifier_set = NullifierSet::new();
|
||||
let private_state = (commitment_set, nullifier_set);
|
||||
|
||||
Self {
|
||||
public_state: HashMap::default(),
|
||||
private_state,
|
||||
programs: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl V03State {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Initializes state with given public account balances leaving other account fields at their
|
||||
/// default values.
|
||||
#[must_use]
|
||||
pub fn with_public_account_balances(
|
||||
mut self,
|
||||
balances: impl IntoIterator<Item = (AccountId, u128)>,
|
||||
) -> Self {
|
||||
let public_accounts = balances.into_iter().map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
});
|
||||
self.public_state.extend(public_accounts);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given public accounts.
|
||||
#[must_use]
|
||||
pub fn with_public_accounts(
|
||||
mut self,
|
||||
public_accounts: impl IntoIterator<Item = (AccountId, Account)>,
|
||||
) -> Self {
|
||||
self.public_state.extend(public_accounts);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given private accounts.
|
||||
#[must_use]
|
||||
pub fn with_private_accounts(
|
||||
mut self,
|
||||
private_accounts: impl IntoIterator<Item = (Commitment, Nullifier)>,
|
||||
) -> Self {
|
||||
let (commitments, nullifiers): (Vec<Commitment>, Vec<Nullifier>) =
|
||||
private_accounts.into_iter().unzip();
|
||||
self.private_state.0.extend(&commitments);
|
||||
self.private_state.1.extend(&nullifiers);
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes state with given builtin programs.
|
||||
#[must_use]
|
||||
pub fn with_programs(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
|
||||
for program in programs {
|
||||
self.insert_program(program);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn insert_program(&mut self, program: Program) {
|
||||
self.programs.insert(program.id(), program);
|
||||
}
|
||||
|
||||
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
|
||||
let StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments,
|
||||
new_nullifiers,
|
||||
program,
|
||||
} = diff.into_state_diff();
|
||||
#[expect(
|
||||
clippy::iter_over_hash_type,
|
||||
reason = "Iteration order doesn't matter here"
|
||||
)]
|
||||
for (account_id, account) in public_diff {
|
||||
*self.get_account_by_id_mut(account_id) = account;
|
||||
}
|
||||
for account_id in signer_account_ids {
|
||||
self.get_account_by_id_mut(account_id)
|
||||
.nonce
|
||||
.public_account_nonce_increment();
|
||||
}
|
||||
self.private_state.0.extend(&new_commitments);
|
||||
self.private_state.1.extend(&new_nullifiers);
|
||||
if let Some(program) = program {
|
||||
self.insert_program(program);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transition_from_public_transaction(
|
||||
&mut self,
|
||||
tx: &PublicTransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff = ValidatedStateDiff::from_public_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transition_from_privacy_preserving_transaction(
|
||||
&mut self,
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff =
|
||||
ValidatedStateDiff::from_privacy_preserving_transaction(tx, self, block_id, timestamp)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transition_from_program_deployment_transaction(
|
||||
&mut self,
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
) -> Result<(), LeeError> {
|
||||
let diff = ValidatedStateDiff::from_program_deployment_transaction(tx, self)?;
|
||||
self.apply_state_diff(diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_account_by_id_mut(&mut self, account_id: AccountId) -> &mut Account {
|
||||
self.public_state.entry(account_id).or_default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_account_by_id(&self, account_id: AccountId) -> Account {
|
||||
self.public_state
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(Account::default)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_proof_for_commitment(&self, commitment: &Commitment) -> Option<MembershipProof> {
|
||||
self.private_state.0.get_proof_for(commitment)
|
||||
}
|
||||
|
||||
pub(crate) const fn programs(&self) -> &HashMap<ProgramId, Program> {
|
||||
&self.programs
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn commitment_set_digest(&self) -> CommitmentSetDigest {
|
||||
self.private_state.0.digest()
|
||||
}
|
||||
|
||||
pub(crate) fn check_commitments_are_new(
|
||||
&self,
|
||||
new_commitments: &[Commitment],
|
||||
) -> Result<(), LeeError> {
|
||||
for commitment in new_commitments {
|
||||
if self.private_state.0.contains(commitment) {
|
||||
return Err(LeeError::InvalidInput("Commitment already seen".to_owned()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn check_nullifiers_are_valid(
|
||||
&self,
|
||||
new_nullifiers: &[(Nullifier, CommitmentSetDigest)],
|
||||
) -> Result<(), LeeError> {
|
||||
for (nullifier, digest) in new_nullifiers {
|
||||
if self.private_state.1.contains(nullifier) {
|
||||
return Err(LeeError::InvalidInput("Nullifier already seen".to_owned()));
|
||||
}
|
||||
if !self.private_state.0.root_history.contains(digest) {
|
||||
return Err(LeeError::InvalidInput(
|
||||
"Unrecognized commitment set digest".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
impl V03State {
|
||||
pub fn force_insert_account(&mut self, account_id: AccountId, account: Account) {
|
||||
self.public_state.insert(account_id, account);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
149
lee/state_machine/src/state/tests/authenticated_transfer.rs
Normal file
149
lee/state_machine/src/state/tests/authenticated_transfer.rs
Normal file
@ -0,0 +1,149 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_default_account_destination() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let from = account_id;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
let balance_to_move = 5;
|
||||
|
||||
let tx = transfer_transaction(from, &key, 0, to, &to_key, 0, balance_to_move);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(from).balance, 95);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 5);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_insuficient_balance() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(account_id, 100)])
|
||||
.with_test_programs();
|
||||
let from = account_id;
|
||||
let from_key = key;
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let balance_to_move = 101;
|
||||
assert!(state.get_account_by_id(from).balance < balance_to_move);
|
||||
|
||||
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::ProgramExecutionFailed(_))));
|
||||
assert_eq!(state.get_account_by_id(from).balance, 100);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 0);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(0));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_authenticated_transfer_program_invocation_non_default_account_destination() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [
|
||||
(
|
||||
account_id1,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
account_id2,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 200,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let from = account_id2;
|
||||
let from_key = key2;
|
||||
let to = account_id1;
|
||||
let to_key = key1;
|
||||
assert_ne!(state.get_account_by_id(to), Account::default());
|
||||
let balance_to_move = 8;
|
||||
|
||||
let tx = transfer_transaction(from, &from_key, 0, to, &to_key, 0, balance_to_move);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(from).balance, 192);
|
||||
assert_eq!(state.get_account_by_id(to).balance, 108);
|
||||
assert_eq!(state.get_account_by_id(from).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(to).nonce, Nonce(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_sequence_of_authenticated_transfer_program_invocations() {
|
||||
let key1 = PrivateKey::try_new([8; 32]).unwrap();
|
||||
let account_id1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let account_id2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let initial_data = [(
|
||||
account_id1,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let key3 = PrivateKey::try_new([3; 32]).unwrap();
|
||||
let account_id3 = AccountId::from(&PublicKey::new_from_private_key(&key3));
|
||||
let balance_to_move = 5;
|
||||
|
||||
let tx = transfer_transaction(
|
||||
account_id1,
|
||||
&key1,
|
||||
0,
|
||||
account_id2,
|
||||
&key2,
|
||||
0,
|
||||
balance_to_move,
|
||||
);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
let balance_to_move = 3;
|
||||
let tx = transfer_transaction(
|
||||
account_id2,
|
||||
&key2,
|
||||
1,
|
||||
account_id3,
|
||||
&key3,
|
||||
0,
|
||||
balance_to_move,
|
||||
);
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id1).balance, 95);
|
||||
assert_eq!(state.get_account_by_id(account_id2).balance, 2);
|
||||
assert_eq!(state.get_account_by_id(account_id3).balance, 3);
|
||||
assert_eq!(state.get_account_by_id(account_id1).nonce, Nonce(1));
|
||||
assert_eq!(state.get_account_by_id(account_id2).nonce, Nonce(2));
|
||||
assert_eq!(state.get_account_by_id(account_id3).nonce, Nonce(1));
|
||||
}
|
||||
118
lee/state_machine/src/state/tests/changer_claimer.rs
Normal file
118
lee/state_machine/src/state/tests/changer_claimer.rs
Normal file
@ -0,0 +1,118 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn public_changer_claimer_no_data_change_no_claim_succeeds() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::changer_claimer().id();
|
||||
// Don't change data (None) and don't claim (false)
|
||||
let instruction: (Option<Vec<u8>>, bool) = (None, false);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should succeed - no changes made, no claim needed
|
||||
assert!(result.is_ok());
|
||||
// Account should remain default/unclaimed
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_changer_claimer_data_change_no_claim_fails() {
|
||||
let initial_data = [];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::changer_claimer().id();
|
||||
// Change data but don't claim (false) - should fail
|
||||
let new_data = vec![1, 2, 3, 4, 5];
|
||||
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim {
|
||||
account_id: err_account_id
|
||||
}
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_changer_claimer_no_data_change_no_claim_succeeds() {
|
||||
let program = crate::test_methods::changer_claimer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let private_account =
|
||||
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
|
||||
// Don't change data (None) and don't claim (false)
|
||||
let instruction: (Option<Vec<u8>>, bool) = (None, false);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
membership_proof: (0, vec![]),
|
||||
identifier: 0,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
// Should succeed - no changes made, no claim needed
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private_changer_claimer_data_change_no_claim_fails() {
|
||||
let program = crate::test_methods::changer_claimer();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let private_account =
|
||||
AccountWithMetadata::new(Account::default(), true, (&sender_keys.npk(), 0));
|
||||
// Change data but don't claim (false) - should fail
|
||||
let new_data = vec![1, 2, 3, 4, 5];
|
||||
let instruction: (Option<Vec<u8>>, bool) = (Some(new_data), false);
|
||||
|
||||
let result = execute_and_prove(
|
||||
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,
|
||||
membership_proof: (0, vec![]),
|
||||
identifier: 0,
|
||||
}],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
// Should fail - cannot modify data without claiming the account
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
1249
lee/state_machine/src/state/tests/circuit.rs
Normal file
1249
lee/state_machine/src/state/tests/circuit.rs
Normal file
File diff suppressed because it is too large
Load Diff
619
lee/state_machine/src/state/tests/claiming.rs
Normal file
619
lee/state_machine/src/state/tests/claiming.rs
Normal file
@ -0,0 +1,619 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let from_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
|
||||
let expected_recipient_post = Account {
|
||||
program_owner: program.id(),
|
||||
balance: amount,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![from, to],
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
amount,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let recipient_post = state.get_account_by_id(to);
|
||||
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_public_account_claiming_fails() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_key = PrivateKey::try_new([9; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![account_id], vec![], 0_u128)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::InvalidProgramBehavior(_))));
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authorized_public_account_claiming_succeeds() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_key = PrivateKey::try_new([10; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&account_key));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
|
||||
assert_eq!(state.get_account_by_id(account_id), Account::default());
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![account_id],
|
||||
vec![Nonce(0)],
|
||||
0_u128,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&account_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(account_id),
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_chained_call() {
|
||||
let program = crate::test_methods::chain_caller();
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
2,
|
||||
None,
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: amount * 2, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
// The `chain_caller` program calls the program twice
|
||||
assert_eq!(from_post.balance, initial_balance - 2 * amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_fails_if_chained_calls_exceeds_depth() {
|
||||
let program = crate::test_methods::chain_caller();
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let from_key = key;
|
||||
let amount: u128 = 0;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
u32::try_from(MAX_NUMBER_CHAINED_CALLS).expect("MAX_NUMBER_CHAINED_CALLS fits in u32") + 1,
|
||||
None,
|
||||
);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
program.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::MaxChainedCallsDepthExceeded)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_that_requires_authentication_of_a_program_derived_account_id_succeeds() {
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let pda_seed = PdaSeed::new([37; 32]);
|
||||
let from = AccountId::for_public_pda(&chain_caller.id(), &pda_seed);
|
||||
let to = AccountId::new([2; 32]);
|
||||
let initial_balance = 1000;
|
||||
let initial_data = [(from, initial_balance), (to, 0)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let amount: u128 = 58;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
1,
|
||||
Some(pda_seed),
|
||||
);
|
||||
|
||||
let expected_to_post = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: amount, // The `chain_caller` chains the program twice
|
||||
..Account::default()
|
||||
};
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism_within_chain_call() {
|
||||
// This test calls the authenticated transfer program through the chain_caller program.
|
||||
// The transfer is made from an initialized sender to an uninitialized recipient. And
|
||||
// it is expected that the recipient account is claimed by the authenticated transfer
|
||||
// program and not the chained_caller program.
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let simple_transfer = crate::test_methods::simple_balance_transfer();
|
||||
let from_key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let from = AccountId::from(&PublicKey::new_from_private_key(&from_key));
|
||||
let initial_balance = 100;
|
||||
let initial_data = [(from, initial_balance)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let to_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let to = AccountId::from(&PublicKey::new_from_private_key(&to_key));
|
||||
let amount: u128 = 37;
|
||||
|
||||
// Check the recipient is an uninitialized account
|
||||
assert_eq!(state.get_account_by_id(to), Account::default());
|
||||
|
||||
let expected_to_post = Account {
|
||||
// The expected program owner is the authenticated transfer program
|
||||
program_owner: simple_transfer.id(),
|
||||
balance: amount,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// The transaction executes the chain_caller program, which internally calls the
|
||||
// authenticated_transfer program
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
1,
|
||||
None,
|
||||
);
|
||||
let message = public_transaction::Message::try_new(
|
||||
chain_caller.id(),
|
||||
vec![to, from], // The chain_caller program permutes the account order in the chain
|
||||
// call
|
||||
vec![Nonce(0), Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&from_key, &to_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
state.transition_from_public_transaction(&tx, 1, 0).unwrap();
|
||||
|
||||
let from_post = state.get_account_by_id(from);
|
||||
let to_post = state.get_account_by_id(to);
|
||||
assert_eq!(from_post.balance, initial_balance - amount);
|
||||
assert_eq!(to_post, expected_to_post);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unauthorized_public_account_claiming_fails_when_executed_privately() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let account_id = AccountId::new([11; 32]);
|
||||
let public_account = AccountWithMetadata::new(Account::default(), false, account_id);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(0_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authorized_public_account_claiming_succeeds_when_executed_privately() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let program_id = program.id();
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: program_id,
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let sender_init_nullifier = Nullifier::for_account_initialization(&sender_account_id);
|
||||
let mut state =
|
||||
V03State::new().with_private_accounts([(sender_commitment.clone(), sender_init_nullifier)]);
|
||||
let sender_pre =
|
||||
AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0));
|
||||
let recipient_private_key = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let recipient_account_id =
|
||||
AccountId::from(&PublicKey::new_from_private_key(&recipient_private_key));
|
||||
let recipient_pre = AccountWithMetadata::new(Account::default(), true, recipient_account_id);
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let balance = 37;
|
||||
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance).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
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.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);
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let nullifier = Nullifier::for_account_update(&sender_commitment, &sender_keys.nsk);
|
||||
assert!(state.private_state.1.contains(&nullifier));
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(recipient_account_id),
|
||||
Account {
|
||||
program_owner: program_id,
|
||||
balance,
|
||||
nonce: Nonce(1),
|
||||
..Account::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test_case::test_case(1; "single call")]
|
||||
#[test_case::test_case(2; "two calls")]
|
||||
fn private_chained_call(number_of_calls: u32) {
|
||||
// Arrange
|
||||
let chain_caller = crate::test_methods::chain_caller();
|
||||
let simple_transfers = crate::test_methods::simple_balance_transfer();
|
||||
let from_keys = test_private_account_keys_1();
|
||||
let to_keys = test_private_account_keys_2();
|
||||
let initial_balance = 100;
|
||||
let from_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
(&from_keys.npk(), 0),
|
||||
);
|
||||
let to_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
(&to_keys.npk(), 0),
|
||||
);
|
||||
|
||||
let from_account_id = AccountId::for_regular_private_account(&from_keys.npk(), 0);
|
||||
let to_account_id = AccountId::for_regular_private_account(&to_keys.npk(), 0);
|
||||
let from_commitment = Commitment::new(&from_account_id, &from_account.account);
|
||||
let to_commitment = Commitment::new(&to_account_id, &to_account.account);
|
||||
let from_init_nullifier = Nullifier::for_account_initialization(&from_account_id);
|
||||
let to_init_nullifier = Nullifier::for_account_initialization(&to_account_id);
|
||||
let mut state = V03State::new()
|
||||
.with_private_accounts([
|
||||
(from_commitment.clone(), from_init_nullifier),
|
||||
(to_commitment.clone(), to_init_nullifier),
|
||||
])
|
||||
.with_test_programs();
|
||||
let amount: u128 = 37;
|
||||
let instruction: (u128, ProgramId, u32, Option<PdaSeed>) = (
|
||||
amount,
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
number_of_calls,
|
||||
None,
|
||||
);
|
||||
|
||||
let (from_ss, from_epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&from_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let (to_ss, to_epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&to_keys.vpk(), &[0_u8; 32], 1);
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(chain_caller, dependencies);
|
||||
|
||||
let from_new_nonce = Nonce::default().private_account_nonce_increment(&from_keys.nsk);
|
||||
let to_new_nonce = Nonce::default().private_account_nonce_increment(&to_keys.nsk);
|
||||
|
||||
let from_expected_post = Account {
|
||||
balance: initial_balance - u128::from(number_of_calls) * amount,
|
||||
nonce: from_new_nonce,
|
||||
..from_account.account.clone()
|
||||
};
|
||||
let from_expected_commitment = Commitment::new(&from_account_id, &from_expected_post);
|
||||
|
||||
let to_expected_post = Account {
|
||||
balance: u128::from(number_of_calls) * amount,
|
||||
nonce: to_new_nonce,
|
||||
..to_account.account.clone()
|
||||
};
|
||||
let to_expected_commitment = Commitment::new(&to_account_id, &to_expected_post);
|
||||
|
||||
// Act
|
||||
let (output, proof) = execute_and_prove(
|
||||
vec![to_account, from_account],
|
||||
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
|
||||
.get_proof_for_commitment(&from_commitment)
|
||||
.expect("from's commitment must be in state"),
|
||||
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
|
||||
.get_proof_for_commitment(&to_commitment)
|
||||
.expect("to's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
)
|
||||
.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);
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&transaction, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
// Assert
|
||||
assert!(
|
||||
state
|
||||
.get_proof_for_commitment(&from_expected_commitment)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
.get_proof_for_commitment(&to_expected_commitment)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claiming_mechanism_cannot_claim_initialied_accounts() {
|
||||
let claimer = crate::test_methods::claimer();
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let account_id = AccountId::new([2; 32]);
|
||||
|
||||
// Insert an account with non-default program owner
|
||||
state.force_insert_account(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(claimer.id(), vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id: err_account_id }
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
/// This test ensures that even if a malicious program tries to perform overflow of balances
|
||||
/// it will not be able to break the balance validation.
|
||||
#[test]
|
||||
fn malicious_program_cannot_break_balance_validation_if_not_in_genesis() {
|
||||
let sender_key = PrivateKey::try_new([37; 32]).unwrap();
|
||||
let sender_id = AccountId::from(&PublicKey::new_from_private_key(&sender_key));
|
||||
let sender_init_balance: u128 = 10;
|
||||
|
||||
let recipient_key = PrivateKey::try_new([42; 32]).unwrap();
|
||||
let recipient_id = AccountId::from(&PublicKey::new_from_private_key(&recipient_key));
|
||||
let recipient_init_balance: u128 = 10;
|
||||
|
||||
let modified_transfer_id = crate::test_methods::modified_transfer_program().id();
|
||||
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts([
|
||||
(
|
||||
sender_id,
|
||||
Account {
|
||||
program_owner: modified_transfer_id,
|
||||
balance: sender_init_balance,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
recipient_id,
|
||||
Account {
|
||||
program_owner: modified_transfer_id,
|
||||
balance: recipient_init_balance,
|
||||
..Account::default()
|
||||
},
|
||||
),
|
||||
])
|
||||
.with_test_programs();
|
||||
|
||||
let balance_to_move: u128 = 4;
|
||||
|
||||
let sender = AccountWithMetadata::new(state.get_account_by_id(sender_id), true, sender_id);
|
||||
|
||||
let sender_nonce = sender.account.nonce;
|
||||
|
||||
let _recipient =
|
||||
AccountWithMetadata::new(state.get_account_by_id(recipient_id), false, sender_id);
|
||||
|
||||
let message = public_transaction::Message::try_new(
|
||||
modified_transfer_id,
|
||||
vec![sender_id, recipient_id],
|
||||
vec![sender_nonce],
|
||||
balance_to_move,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let res = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
let expected_total_balance_pre_states =
|
||||
WrappedBalanceSum::from_balances([sender_init_balance, recipient_init_balance].into_iter())
|
||||
.unwrap();
|
||||
let expected_total_balance_post_states = WrappedBalanceSum::from_balances(
|
||||
[sender_init_balance, recipient_init_balance, u128::MAX, 1].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
res,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
)
|
||||
)) if total_balance_pre_states == expected_total_balance_pre_states && total_balance_post_states == expected_total_balance_post_states
|
||||
));
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_id);
|
||||
let recipient_post = state.get_account_by_id(recipient_id);
|
||||
|
||||
let expected_sender_post = {
|
||||
let mut this = state.get_account_by_id(sender_id);
|
||||
this.balance = sender_init_balance;
|
||||
this.nonce = Nonce(0);
|
||||
this
|
||||
};
|
||||
|
||||
let expected_recipient_post = {
|
||||
let mut this = state.get_account_by_id(sender_id);
|
||||
this.balance = recipient_init_balance;
|
||||
this.nonce = Nonce(0);
|
||||
this
|
||||
};
|
||||
|
||||
assert_eq!(expected_sender_post, sender_post);
|
||||
assert_eq!(expected_recipient_post, recipient_post);
|
||||
}
|
||||
235
lee/state_machine/src/state/tests/flash_swap.rs
Normal file
235
lee/state_machine/src/state/tests/flash_swap.rs
Normal file
@ -0,0 +1,235 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flash_swap_successful() {
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
// Callback instruction: return funds
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: true,
|
||||
token_program_id: token.id(),
|
||||
amount: amount_out,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(result.is_ok(), "flash swap should succeed: {result:?}");
|
||||
|
||||
// Vault balance restored, receiver back to 0
|
||||
assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance);
|
||||
assert_eq!(state.get_account_by_id(receiver_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_callback_keeps_funds_rollback() {
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
let amount_out: u128 = 100;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
// Callback instruction: do NOT return funds
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: false,
|
||||
token_program_id: token.id(),
|
||||
amount: amount_out,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
// Invariant check fails → entire tx rolls back
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"flash swap should fail when callback keeps funds"
|
||||
);
|
||||
|
||||
// State unchanged (rollback)
|
||||
assert_eq!(state.get_account_by_id(vault_id).balance, initial_balance);
|
||||
assert_eq!(state.get_account_by_id(receiver_id).balance, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_self_call_targets_correct_program() {
|
||||
// Zero-amount flash swap: the invariant self-call still runs and succeeds
|
||||
// because vault balance doesn't decrease.
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let callback = crate::test_methods::flash_swap_callback();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
let receiver_id = AccountId::for_public_pda(&callback.id(), &PdaSeed::new([1_u8; 32]));
|
||||
|
||||
let initial_balance: u128 = 1000;
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: initial_balance,
|
||||
..Account::default()
|
||||
};
|
||||
let receiver_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
state.force_insert_account(receiver_id, receiver_account);
|
||||
|
||||
let cb_instruction = CallbackInstruction {
|
||||
return_funds: true,
|
||||
token_program_id: token.id(),
|
||||
amount: 0,
|
||||
};
|
||||
let cb_data = Program::serialize_instruction(cb_instruction).unwrap();
|
||||
|
||||
let instruction = FlashSwapInstruction::Initiate {
|
||||
token_program_id: token.id(),
|
||||
callback_program_id: callback.id(),
|
||||
amount_out: 0,
|
||||
callback_instruction_data: cb_data,
|
||||
};
|
||||
|
||||
let tx = build_flash_swap_tx(&initiator, vault_id, receiver_id, instruction);
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"zero-amount flash swap should succeed: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flash_swap_standalone_invariant_check_rejected() {
|
||||
// Calling InvariantCheck directly (not as a chained self-call) should fail
|
||||
// because caller_program_id will be None.
|
||||
let initiator = crate::test_methods::flash_swap_initiator();
|
||||
let token = crate::test_methods::simple_balance_transfer();
|
||||
|
||||
let vault_id = AccountId::for_public_pda(&initiator.id(), &PdaSeed::new([0_u8; 32]));
|
||||
|
||||
let vault_account = Account {
|
||||
program_owner: token.id(),
|
||||
balance: 1000,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(vault_id, vault_account);
|
||||
|
||||
let instruction = FlashSwapInstruction::InvariantCheck {
|
||||
min_vault_balance: 1000,
|
||||
};
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(initiator.id(), vec![vault_id], vec![], instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"standalone InvariantCheck should be rejected (caller_program_id is None)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_self_program_id_rejected_in_public_execution() {
|
||||
let program = crate::test_methods::malicious_self_program_id();
|
||||
let acc_id = AccountId::new([99; 32]);
|
||||
let account = Account::default();
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(acc_id, account);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"program with wrong self_program_id in output should be rejected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_caller_program_id_rejected_in_public_execution() {
|
||||
let program = crate::test_methods::malicious_caller_program_id();
|
||||
let acc_id = AccountId::new([99; 32]);
|
||||
let account = Account::default();
|
||||
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
state.force_insert_account(acc_id, account);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program.id(), vec![acc_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"program with spoofed caller_program_id in output should be rejected"
|
||||
);
|
||||
}
|
||||
128
lee/state_machine/src/state/tests/genesis.rs
Normal file
128
lee/state_machine/src/state/tests/genesis.rs
Normal file
@ -0,0 +1,128 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_works() {
|
||||
let key1 = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let key2 = PrivateKey::try_new([2; 32]).unwrap();
|
||||
let addr1 = AccountId::from(&PublicKey::new_from_private_key(&key1));
|
||||
let addr2 = AccountId::from(&PublicKey::new_from_private_key(&key2));
|
||||
let expected_public_state = {
|
||||
let mut this = HashMap::new();
|
||||
this.insert(
|
||||
addr1,
|
||||
Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this.insert(
|
||||
addr2,
|
||||
Account {
|
||||
balance: 151,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
this
|
||||
};
|
||||
let expected_builtin_programs = HashMap::new();
|
||||
|
||||
let state =
|
||||
V03State::new().with_public_account_balances([(addr1, 100_u128), (addr2, 151_u128)]);
|
||||
|
||||
assert_eq!(state.public_state, expected_public_state);
|
||||
assert_eq!(state.programs, expected_builtin_programs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_includes_nullifiers_for_private_accounts() {
|
||||
let keys1 = test_private_account_keys_1();
|
||||
let keys2 = test_private_account_keys_2();
|
||||
|
||||
let account = Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
let account_id1 = AccountId::for_regular_private_account(&keys1.npk(), 0);
|
||||
let account_id2 = AccountId::for_regular_private_account(&keys2.npk(), 0);
|
||||
|
||||
let init_commitment1 = Commitment::new(&account_id1, &account);
|
||||
let init_commitment2 = Commitment::new(&account_id2, &account);
|
||||
let init_nullifier1 = Nullifier::for_account_initialization(&account_id1);
|
||||
let init_nullifier2 = Nullifier::for_account_initialization(&account_id2);
|
||||
|
||||
let initial_private_accounts = vec![
|
||||
(init_commitment1, init_nullifier1),
|
||||
(init_commitment2, init_nullifier2),
|
||||
];
|
||||
|
||||
let state = V03State::new().with_private_accounts(initial_private_accounts);
|
||||
|
||||
assert!(state.private_state.1.contains(&init_nullifier1));
|
||||
assert!(state.private_state.1.contains(&init_nullifier2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_program() {
|
||||
let mut state = V03State::new();
|
||||
let program_to_insert = crate::test_methods::simple_balance_transfer();
|
||||
let program_id = program_to_insert.id();
|
||||
assert!(!state.programs.contains_key(&program_id));
|
||||
|
||||
state.insert_program(program_to_insert);
|
||||
|
||||
assert!(state.programs.contains_key(&program_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_account_by_account_id_non_default_account() {
|
||||
let key = PrivateKey::try_new([1; 32]).unwrap();
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(&key));
|
||||
let initial_data = [(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let state = V03State::new().with_public_accounts(initial_data);
|
||||
let expected_account = &state.public_state[&account_id];
|
||||
|
||||
let account = state.get_account_by_id(account_id);
|
||||
|
||||
assert_eq!(&account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_account_by_account_id_default_account() {
|
||||
let addr2 = AccountId::new([0; 32]);
|
||||
let state = V03State::new();
|
||||
let expected_account = Account::default();
|
||||
|
||||
let account = state.get_account_by_id(addr2);
|
||||
|
||||
assert_eq!(account, expected_account);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_programs_getter() {
|
||||
let state = V03State::new();
|
||||
|
||||
let builtin_programs = state.programs();
|
||||
|
||||
assert_eq!(builtin_programs, &state.programs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_serialization_roundtrip() {
|
||||
let account_id_1 = AccountId::new([1; 32]);
|
||||
let account_id_2 = AccountId::new([2; 32]);
|
||||
let initial_data = [(account_id_1, 100_u128), (account_id_2, 151_u128)];
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&initial_data))
|
||||
.with_test_programs();
|
||||
let bytes = borsh::to_vec(&state).unwrap();
|
||||
let state_from_bytes: V03State = borsh::from_slice(&bytes).unwrap();
|
||||
assert_eq!(state, state_from_bytes);
|
||||
}
|
||||
444
lee/state_machine/src/state/tests/mod.rs
Normal file
444
lee/state_machine/src/state/tests/mod.rs
Normal file
@ -0,0 +1,444 @@
|
||||
#![expect(
|
||||
clippy::arithmetic_side_effects,
|
||||
clippy::shadow_unrelated,
|
||||
reason = "We don't care about it in tests"
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, DUMMY_COMMITMENT_HASH, EncryptedAccountData, InputAccountIdentity,
|
||||
Nullifier, NullifierPublicKey, NullifierSecretKey, SharedSecretKey, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata, Nonce, data::Data},
|
||||
encryption::{EphemeralPublicKey, ML_KEM_768_CIPHERTEXT_LEN, ViewingPublicKey},
|
||||
program::{
|
||||
BlockValidityWindow, ExecutionValidationError, MAX_NUMBER_CHAINED_CALLS, PdaSeed,
|
||||
ProgramId, TimestampValidityWindow, WrappedBalanceSum,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PublicKey, PublicTransaction, V03State,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
execute_and_prove,
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::ProgramWithDependencies, message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
program::Program,
|
||||
public_transaction,
|
||||
signature::PrivateKey,
|
||||
};
|
||||
|
||||
mod authenticated_transfer;
|
||||
mod changer_claimer;
|
||||
mod circuit;
|
||||
mod claiming;
|
||||
mod flash_swap;
|
||||
mod genesis;
|
||||
mod privacy_preserving;
|
||||
mod public_program_rules;
|
||||
mod validity_window;
|
||||
|
||||
impl V03State {
|
||||
/// Include test programs in the builtin programs map.
|
||||
#[must_use]
|
||||
pub fn with_test_programs(mut self) -> Self {
|
||||
self.insert_program(crate::test_methods::simple_balance_transfer());
|
||||
self.insert_program(crate::test_methods::nonce_changer());
|
||||
self.insert_program(crate::test_methods::extra_output());
|
||||
self.insert_program(crate::test_methods::missing_output());
|
||||
self.insert_program(crate::test_methods::program_owner_changer());
|
||||
self.insert_program(crate::test_methods::data_changer());
|
||||
self.insert_program(crate::test_methods::minter());
|
||||
self.insert_program(crate::test_methods::burner());
|
||||
self.insert_program(crate::test_methods::auth_asserting_noop());
|
||||
self.insert_program(crate::test_methods::private_pda_delegator());
|
||||
self.insert_program(crate::test_methods::pda_claimer());
|
||||
self.insert_program(crate::test_methods::two_pda_claimer());
|
||||
self.insert_program(crate::test_methods::noop());
|
||||
self.insert_program(crate::test_methods::chain_caller());
|
||||
self.insert_program(crate::test_methods::modified_transfer_program());
|
||||
self.insert_program(crate::test_methods::malicious_authorization_changer());
|
||||
self.insert_program(crate::test_methods::validity_window());
|
||||
self.insert_program(crate::test_methods::flash_swap_initiator());
|
||||
self.insert_program(crate::test_methods::flash_swap_callback());
|
||||
self.insert_program(crate::test_methods::malicious_self_program_id());
|
||||
self.insert_program(crate::test_methods::malicious_caller_program_id());
|
||||
self.insert_program(crate::test_methods::pda_spend_proxy());
|
||||
self.insert_program(crate::test_methods::claimer());
|
||||
self.insert_program(crate::test_methods::changer_claimer());
|
||||
self.insert_program(crate::test_methods::validity_window_chain_caller());
|
||||
self.insert_program(crate::test_methods::simple_transfer_proxy());
|
||||
self.insert_program(crate::test_methods::malicious_injector());
|
||||
self.insert_program(crate::test_methods::malicious_launderer());
|
||||
self.insert_program(crate::test_methods::modified_transfer_program());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_non_default_accounts_but_default_program_owners(mut self) -> Self {
|
||||
let account_with_default_values_except_balance = Account {
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let account_with_default_values_except_nonce = Account {
|
||||
nonce: Nonce(37),
|
||||
..Account::default()
|
||||
};
|
||||
let account_with_default_values_except_data = Account {
|
||||
data: vec![0xca, 0xfe].try_into().unwrap(),
|
||||
..Account::default()
|
||||
};
|
||||
self.force_insert_account(
|
||||
AccountId::new([255; 32]),
|
||||
account_with_default_values_except_balance,
|
||||
);
|
||||
self.force_insert_account(
|
||||
AccountId::new([254; 32]),
|
||||
account_with_default_values_except_nonce,
|
||||
);
|
||||
self.force_insert_account(
|
||||
AccountId::new([253; 32]),
|
||||
account_with_default_values_except_data,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_account_owned_by_burner_program(mut self) -> Self {
|
||||
let account = Account {
|
||||
program_owner: crate::test_methods::burner().id(),
|
||||
balance: 100,
|
||||
..Default::default()
|
||||
};
|
||||
self.force_insert_account(AccountId::new([252; 32]), account);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_private_account(mut self, keys: &TestPrivateKeys, account: &Account) -> Self {
|
||||
let account_id = AccountId::for_regular_private_account(&keys.npk(), 0);
|
||||
let commitment = Commitment::new(&account_id, account);
|
||||
self.private_state.0.extend(&[commitment]);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPublicKeys {
|
||||
pub signing_key: PrivateKey,
|
||||
}
|
||||
|
||||
impl TestPublicKeys {
|
||||
pub fn account_id(&self) -> AccountId {
|
||||
AccountId::from(&PublicKey::new_from_private_key(&self.signing_key))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestPrivateKeys {
|
||||
pub nsk: NullifierSecretKey,
|
||||
pub d: [u8; 32],
|
||||
pub z: [u8; 32],
|
||||
}
|
||||
|
||||
impl TestPrivateKeys {
|
||||
pub fn npk(&self) -> NullifierPublicKey {
|
||||
NullifierPublicKey::from(&self.nsk)
|
||||
}
|
||||
|
||||
pub fn vpk(&self) -> ViewingPublicKey {
|
||||
ViewingPublicKey::from_seed(&self.d, &self.z)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flash Swap types (mirrors of guest types for host-side serialisation) ──
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct CallbackInstruction {
|
||||
return_funds: bool,
|
||||
token_program_id: ProgramId,
|
||||
amount: u128,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
enum FlashSwapInstruction {
|
||||
Initiate {
|
||||
token_program_id: ProgramId,
|
||||
callback_program_id: ProgramId,
|
||||
amount_out: u128,
|
||||
callback_instruction_data: Vec<u32>,
|
||||
},
|
||||
InvariantCheck {
|
||||
min_vault_balance: u128,
|
||||
},
|
||||
}
|
||||
|
||||
fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<AccountId, Account> {
|
||||
initial_data
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn transfer_transaction(
|
||||
from: AccountId,
|
||||
from_key: &PrivateKey,
|
||||
from_nonce: u128,
|
||||
to: AccountId,
|
||||
to_key: &PrivateKey,
|
||||
to_nonce: u128,
|
||||
balance: u128,
|
||||
) -> PublicTransaction {
|
||||
let account_ids = vec![from, to];
|
||||
let nonces = vec![Nonce(from_nonce), Nonce(to_nonce)];
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, balance).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[from_key, to_key]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn build_flash_swap_tx(
|
||||
initiator: &Program,
|
||||
vault_id: AccountId,
|
||||
receiver_id: AccountId,
|
||||
instruction: FlashSwapInstruction,
|
||||
) -> PublicTransaction {
|
||||
let message = public_transaction::Message::try_new(
|
||||
initiator.id(),
|
||||
vec![vault_id, receiver_id],
|
||||
vec![], // no signers — vault is PDA-authorised
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn test_public_account_keys_1() -> TestPublicKeys {
|
||||
TestPublicKeys {
|
||||
signing_key: PrivateKey::try_new([37; 32]).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_public_account_keys_2() -> TestPublicKeys {
|
||||
TestPublicKeys {
|
||||
signing_key: PrivateKey::try_new([38; 32]).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_private_account_keys_1() -> TestPrivateKeys {
|
||||
TestPrivateKeys {
|
||||
nsk: [13; 32],
|
||||
d: [31; 32],
|
||||
z: [32; 32],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_private_account_keys_2() -> TestPrivateKeys {
|
||||
TestPrivateKeys {
|
||||
nsk: [38; 32],
|
||||
d: [83; 32],
|
||||
z: [84; 32],
|
||||
}
|
||||
}
|
||||
|
||||
fn shielded_balance_transfer_for_tests(
|
||||
sender_keys: &TestPublicKeys,
|
||||
recipient_keys: &TestPrivateKeys,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let sender = AccountWithMetadata::new(
|
||||
state.get_account_by_id(sender_keys.account_id()),
|
||||
true,
|
||||
sender_keys.account_id(),
|
||||
);
|
||||
|
||||
let sender_nonce = sender.account.nonce;
|
||||
|
||||
let recipient = AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
|
||||
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender, recipient],
|
||||
Program::serialize_instruction(balance_to_move).unwrap(),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&crate::test_methods::simple_balance_transfer().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![sender_keys.account_id()],
|
||||
vec![sender_nonce],
|
||||
output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[&sender_keys.signing_key]);
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn private_balance_transfer_for_tests(
|
||||
sender_keys: &TestPrivateKeys,
|
||||
sender_private_account: &Account,
|
||||
recipient_keys: &TestPrivateKeys,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
true,
|
||||
(&sender_keys.npk(), 0),
|
||||
);
|
||||
let recipient_pre =
|
||||
AccountWithMetadata::new(Account::default(), false, (&recipient_keys.npk(), 0));
|
||||
|
||||
let (shared_secret_1, epk_1) =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let (shared_secret_2, epk_2) =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 1);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).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
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
},
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.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)
|
||||
}
|
||||
|
||||
fn deshielded_balance_transfer_for_tests(
|
||||
sender_keys: &TestPrivateKeys,
|
||||
sender_private_account: &Account,
|
||||
recipient_account_id: &AccountId,
|
||||
balance_to_move: u128,
|
||||
state: &V03State,
|
||||
) -> PrivacyPreservingTransaction {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let sender_commitment = Commitment::new(&sender_account_id, sender_private_account);
|
||||
let sender_pre = AccountWithMetadata::new(
|
||||
sender_private_account.clone(),
|
||||
true,
|
||||
(&sender_keys.npk(), 0),
|
||||
);
|
||||
let recipient_pre = AccountWithMetadata::new(
|
||||
state.get_account_by_id(*recipient_account_id),
|
||||
false,
|
||||
*recipient_account_id,
|
||||
);
|
||||
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&sender_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
vec![sender_pre, recipient_pre],
|
||||
Program::serialize_instruction(balance_to_move).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
|
||||
.get_proof_for_commitment(&sender_commitment)
|
||||
.expect("sender's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public,
|
||||
],
|
||||
&program.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let message =
|
||||
Message::try_from_circuit_output(vec![*recipient_account_id], vec![], output).unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]);
|
||||
|
||||
PrivacyPreservingTransaction::new(message, witness_set)
|
||||
}
|
||||
|
||||
fn valid_private_transfer_tx_and_state() -> (V03State, PrivacyPreservingTransaction) {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: Nonce(0xdead_beef),
|
||||
..Account::default()
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
let state = V03State::new().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)
|
||||
}
|
||||
539
lee/state_machine/src/state/tests/privacy_preserving.rs
Normal file
539
lee/state_machine/src/state/tests/privacy_preserving.rs
Normal file
@ -0,0 +1,539 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_shielded() {
|
||||
let sender_keys = test_public_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let mut state = V03State::new().with_public_accounts([(
|
||||
sender_keys.account_id(),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 200,
|
||||
..Account::default()
|
||||
},
|
||||
)]);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let tx =
|
||||
shielded_balance_transfer_for_tests(&sender_keys, &recipient_keys, balance_to_move, &state);
|
||||
|
||||
let expected_sender_post = {
|
||||
let mut this = state.get_account_by_id(sender_keys.account_id());
|
||||
this.balance -= balance_to_move;
|
||||
this.nonce.public_account_nonce_increment();
|
||||
this
|
||||
};
|
||||
|
||||
let [expected_new_commitment] = tx.message().new_commitments.clone().try_into().unwrap();
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let sender_post = state.get_account_by_id(sender_keys.account_id());
|
||||
assert_eq!(sender_post, expected_sender_post);
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment));
|
||||
|
||||
assert_eq!(
|
||||
state.get_account_by_id(sender_keys.account_id()).balance,
|
||||
200 - balance_to_move
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transition_from_privacy_preserving_transaction_private() {
|
||||
let sender_keys = test_private_account_keys_1();
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
data: Data::default(),
|
||||
};
|
||||
let recipient_keys = test_private_account_keys_2();
|
||||
|
||||
let mut state = V03State::new().with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let tx = private_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys,
|
||||
balance_to_move,
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let expected_new_commitment_1 = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
balance: sender_private_account.balance - balance_to_move,
|
||||
data: Data::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let expected_new_nullifier =
|
||||
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
|
||||
|
||||
let expected_new_commitment_2 = Commitment::new(
|
||||
&recipient_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: Nonce::private_account_nonce_init(&recipient_account_id),
|
||||
balance: balance_to_move,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
|
||||
let previous_public_state = state.public_state.clone();
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment_1));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment_2));
|
||||
assert!(!state.private_state.1.contains(&expected_new_nullifier));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.public_state, previous_public_state);
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment_1));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment_2));
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
}
|
||||
|
||||
/// 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();
|
||||
let sender_nonce = Nonce(0xdead_beef);
|
||||
|
||||
let sender_private_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
nonce: sender_nonce,
|
||||
data: Data::default(),
|
||||
};
|
||||
let recipient_keys = test_public_account_keys_1();
|
||||
let recipient_initial_balance = 400;
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts([(
|
||||
recipient_keys.account_id(),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: recipient_initial_balance,
|
||||
..Account::default()
|
||||
},
|
||||
)])
|
||||
.with_private_account(&sender_keys, &sender_private_account);
|
||||
|
||||
let balance_to_move = 37;
|
||||
|
||||
let expected_recipient_post = {
|
||||
let mut this = state.get_account_by_id(recipient_keys.account_id());
|
||||
this.balance += balance_to_move;
|
||||
this
|
||||
};
|
||||
|
||||
let tx = deshielded_balance_transfer_for_tests(
|
||||
&sender_keys,
|
||||
&sender_private_account,
|
||||
&recipient_keys.account_id(),
|
||||
balance_to_move,
|
||||
&state,
|
||||
);
|
||||
|
||||
let sender_account_id = AccountId::for_regular_private_account(&sender_keys.npk(), 0);
|
||||
let expected_new_commitment = Commitment::new(
|
||||
&sender_account_id,
|
||||
&Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
nonce: sender_nonce.private_account_nonce_increment(&sender_keys.nsk),
|
||||
balance: sender_private_account.balance - balance_to_move,
|
||||
data: Data::default(),
|
||||
},
|
||||
);
|
||||
|
||||
let sender_pre_commitment = Commitment::new(&sender_account_id, &sender_private_account);
|
||||
let expected_new_nullifier =
|
||||
Nullifier::for_account_update(&sender_pre_commitment, &sender_keys.nsk);
|
||||
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(!state.private_state.0.contains(&expected_new_commitment));
|
||||
assert!(!state.private_state.1.contains(&expected_new_nullifier));
|
||||
|
||||
state
|
||||
.transition_from_privacy_preserving_transaction(&tx, 1, 0)
|
||||
.unwrap();
|
||||
|
||||
let recipient_post = state.get_account_by_id(recipient_keys.account_id());
|
||||
assert_eq!(recipient_post, expected_recipient_post);
|
||||
assert!(state.private_state.0.contains(&sender_pre_commitment));
|
||||
assert!(state.private_state.0.contains(&expected_new_commitment));
|
||||
assert!(state.private_state.1.contains(&expected_new_nullifier));
|
||||
assert_eq!(
|
||||
state.get_account_by_id(recipient_keys.account_id()).balance,
|
||||
recipient_initial_balance + balance_to_move
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burner_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::burner();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minter_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::minter();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonce_changer_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::nonce_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_changer_program_should_fail_for_non_owned_account_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::data_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(vec![0]).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_changer_program_should_fail_for_too_large_data_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::data_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let large_data: Vec<u8> =
|
||||
vec![
|
||||
0;
|
||||
usize::try_from(lee_core::account::data::DATA_MAX_LENGTH.as_u64())
|
||||
.expect("DATA_MAX_LENGTH fits in usize")
|
||||
+ 1
|
||||
];
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(large_data).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::ProgramProveFailed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extra_output_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::extra_output();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_output_program_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::missing_output();
|
||||
let public_account_1 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let public_account_2 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([1; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account_1, public_account_2],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_owner_changer_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::program_owner_changer();
|
||||
let public_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account],
|
||||
Program::serialize_instruction(()).unwrap(),
|
||||
vec![InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_from_non_owned_account_should_fail_in_privacy_preserving_circuit() {
|
||||
let program = crate::test_methods::simple_balance_transfer();
|
||||
let public_account_1 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([0; 32]),
|
||||
);
|
||||
let public_account_2 = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: program.id(),
|
||||
balance: 0,
|
||||
..Account::default()
|
||||
},
|
||||
true,
|
||||
AccountId::new([1; 32]),
|
||||
);
|
||||
|
||||
let result = execute_and_prove(
|
||||
vec![public_account_1, public_account_2],
|
||||
Program::serialize_instruction(10_u128).unwrap(),
|
||||
vec![InputAccountIdentity::Public, InputAccountIdentity::Public],
|
||||
&program.into(),
|
||||
);
|
||||
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malicious_authorization_changer_should_fail_in_privacy_preserving_circuit() {
|
||||
// Arrange
|
||||
let malicious_program = crate::test_methods::malicious_authorization_changer();
|
||||
let simple_transfers = crate::test_methods::simple_balance_transfer();
|
||||
let sender_keys = test_public_account_keys_1();
|
||||
let recipient_keys = test_private_account_keys_1();
|
||||
|
||||
let sender_account = AccountWithMetadata::new(
|
||||
Account {
|
||||
program_owner: simple_transfers.id(),
|
||||
balance: 100,
|
||||
..Default::default()
|
||||
},
|
||||
false,
|
||||
sender_keys.account_id(),
|
||||
);
|
||||
let recipient_account =
|
||||
AccountWithMetadata::new(Account::default(), true, (&recipient_keys.npk(), 0));
|
||||
|
||||
let recipient_account_id = AccountId::for_regular_private_account(&recipient_keys.npk(), 0);
|
||||
let recipient_commitment = Commitment::new(&recipient_account_id, &recipient_account.account);
|
||||
let recipient_init_nullifier = Nullifier::for_account_initialization(&recipient_account_id);
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(
|
||||
sender_account.account_id,
|
||||
sender_account.account.balance,
|
||||
)]))
|
||||
.with_private_accounts([(recipient_commitment.clone(), recipient_init_nullifier)])
|
||||
.with_test_programs();
|
||||
|
||||
let balance_to_transfer = 10_u128;
|
||||
let instruction = (balance_to_transfer, simple_transfers.id());
|
||||
|
||||
let recipient =
|
||||
SharedSecretKey::encapsulate_deterministic(&recipient_keys.vpk(), &[0_u8; 32], 0).0;
|
||||
|
||||
let mut dependencies = HashMap::new();
|
||||
dependencies.insert(simple_transfers.id(), simple_transfers);
|
||||
let program_with_deps = ProgramWithDependencies::new(malicious_program, dependencies);
|
||||
|
||||
// Act - execute the malicious program - this should fail during proving
|
||||
let result = execute_and_prove(
|
||||
vec![sender_account, recipient_account],
|
||||
Program::serialize_instruction(instruction).unwrap(),
|
||||
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
|
||||
.get_proof_for_commitment(&recipient_commitment)
|
||||
.expect("recipient's commitment must be in state"),
|
||||
identifier: 0,
|
||||
},
|
||||
],
|
||||
&program_with_deps,
|
||||
);
|
||||
|
||||
// Assert - should fail because the malicious program tries to manipulate is_authorized
|
||||
assert!(matches!(result, Err(LeeError::CircuitProvingError(_))));
|
||||
}
|
||||
325
lee/state_machine/src/state/tests/public_program_rules.rs
Normal file
325
lee/state_machine/src/state/tests/public_program_rules.rs
Normal file
@ -0,0 +1,325 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_nonces() {
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(account_id, 100)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![account_id];
|
||||
let program_id = crate::test_methods::nonce_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedNonce { account_id: err_account_id }
|
||||
)
|
||||
)) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_output_accounts_exceed_inputs() {
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(AccountId::new([1; 32]), 0)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32])];
|
||||
let program_id = crate::test_methods::extra_output().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 1 && post_state_length == 2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_with_missing_output_accounts() {
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(AccountId::new([1; 32]), 100)])
|
||||
.with_test_programs();
|
||||
let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])];
|
||||
let program_id = crate::test_methods::missing_output().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedPreStatePostStateLength {
|
||||
pre_state_length,
|
||||
post_state_length
|
||||
}
|
||||
)
|
||||
)) if pre_state_length == 2 && post_state_length == 1
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_program_owner() {
|
||||
let initial_data = [(
|
||||
AccountId::new([1; 32]),
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
..Account::default()
|
||||
},
|
||||
)];
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in the program owner
|
||||
// field
|
||||
assert_ne!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_balance() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in balance field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_ne!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_nonce() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([254; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in nonce field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_ne!(account.nonce, Account::default().nonce);
|
||||
assert_eq!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_program_owner_with_only_non_default_data() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([253; 32]);
|
||||
let account = state.get_account_by_id(account_id);
|
||||
// Assert the target account only differs from the default account in data field
|
||||
assert_eq!(account.program_owner, Account::default().program_owner);
|
||||
assert_eq!(account.balance, Account::default().balance);
|
||||
assert_eq!(account.nonce, Account::default().nonce);
|
||||
assert_ne!(account.data, Account::default().data);
|
||||
let program_id = crate::test_methods::program_owner_changer().id();
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::ModifiedProgramOwner { account_id: err_account_id }
|
||||
))) if err_account_id == account_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_transfers_balance_from_non_owned_account() {
|
||||
let sender_account_id = AccountId::new([1; 32]);
|
||||
let receiver_account_id = AccountId::new([2; 32]);
|
||||
let mut state = V03State::new()
|
||||
.with_public_account_balances([(sender_account_id, 100)])
|
||||
.with_test_programs();
|
||||
let balance_to_move: u128 = 1;
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
assert_ne!(
|
||||
state.get_account_by_id(sender_account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let message = public_transaction::Message::try_new(
|
||||
program_id,
|
||||
vec![sender_account_id, receiver_account_id],
|
||||
vec![],
|
||||
balance_to_move,
|
||||
)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedBalanceDecrease { account_id: err_account_id, owner_program_id, executing_program_id }
|
||||
))) if err_account_id == sender_account_id && owner_program_id != program_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_modifies_data_of_non_owned_account() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_non_default_accounts_but_default_program_owners();
|
||||
let account_id = AccountId::new([255; 32]);
|
||||
let program_id = crate::test_methods::data_changer().id();
|
||||
|
||||
assert_ne!(state.get_account_by_id(account_id), Account::default());
|
||||
assert_ne!(
|
||||
state.get_account_by_id(account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], vec![0])
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 1, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::UnauthorizedDataModification { account_id: err_account_id, executing_program_id }
|
||||
))) if err_account_id == account_id && executing_program_id == program_id
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_minting() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs();
|
||||
let account_id = AccountId::new([1; 32]);
|
||||
let program_id = crate::test_methods::minter().id();
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], ()).unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 0.into() && total_balance_post_states == 1.into()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_should_fail_if_does_not_preserve_total_balance_by_burning() {
|
||||
let initial_data = HashMap::new();
|
||||
let mut state = V03State::new()
|
||||
.with_public_accounts(initial_data)
|
||||
.with_test_programs()
|
||||
.with_account_owned_by_burner_program();
|
||||
let program_id = crate::test_methods::burner().id();
|
||||
let account_id = AccountId::new([252; 32]);
|
||||
assert_eq!(
|
||||
state.get_account_by_id(account_id).program_owner,
|
||||
program_id
|
||||
);
|
||||
let balance_to_burn: u128 = 1;
|
||||
assert!(state.get_account_by_id(account_id).balance > balance_to_burn);
|
||||
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, vec![account_id], vec![], balance_to_burn)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
let tx = PublicTransaction::new(message, witness_set);
|
||||
let result = state.transition_from_public_transaction(&tx, 2, 0);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(InvalidProgramBehaviorError::ExecutionValidationFailed(
|
||||
ExecutionValidationError::MismatchedTotalBalance { total_balance_pre_states, total_balance_post_states }
|
||||
))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into()
|
||||
));
|
||||
}
|
||||
243
lee/state_machine/src/state/tests/validity_window.rs
Normal file
243
lee/state_machine/src/state/tests/validity_window.rs
Normal file
@ -0,0 +1,243 @@
|
||||
use super::*;
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn validity_window_works_in_public_transactions(
|
||||
validity_window: (Option<BlockId>, Option<BlockId>),
|
||||
block_id: BlockId,
|
||||
) {
|
||||
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_public_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
let program_id = validity_window_program.id();
|
||||
let instruction = (
|
||||
block_validity_window,
|
||||
TimestampValidityWindow::new_unbounded(),
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_public_transaction(&tx, block_id, 0);
|
||||
let is_inside_validity_window =
|
||||
match (block_validity_window.start(), block_validity_window.end()) {
|
||||
(Some(s), Some(e)) => s <= block_id && block_id < e,
|
||||
(Some(s), None) => s <= block_id,
|
||||
(None, Some(e)) => block_id < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn timestamp_validity_window_works_in_public_transactions(
|
||||
validity_window: (Option<Timestamp>, Option<Timestamp>),
|
||||
timestamp: Timestamp,
|
||||
) {
|
||||
let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_public_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, account_keys.account_id());
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let account_ids = vec![pre.account_id];
|
||||
let nonces = vec![];
|
||||
let program_id = validity_window_program.id();
|
||||
let instruction = (
|
||||
BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window,
|
||||
);
|
||||
let message =
|
||||
public_transaction::Message::try_new(program_id, account_ids, nonces, instruction)
|
||||
.unwrap();
|
||||
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
|
||||
PublicTransaction::new(message, witness_set)
|
||||
};
|
||||
let result = state.transition_from_public_transaction(&tx, 1, timestamp);
|
||||
let is_inside_validity_window = match (
|
||||
timestamp_validity_window.start(),
|
||||
timestamp_validity_window.end(),
|
||||
) {
|
||||
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
|
||||
(Some(s), None) => s <= timestamp,
|
||||
(None, Some(e)) => timestamp < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn validity_window_works_in_privacy_preserving_transactions(
|
||||
validity_window: (Option<BlockId>, Option<BlockId>),
|
||||
block_id: BlockId,
|
||||
) {
|
||||
let block_validity_window: BlockValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let instruction = (
|
||||
block_validity_window,
|
||||
TimestampValidityWindow::new_unbounded(),
|
||||
);
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&validity_window_program.into(),
|
||||
)
|
||||
.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)
|
||||
};
|
||||
let result = state.transition_from_privacy_preserving_transaction(&tx, block_id, 0);
|
||||
let is_inside_validity_window =
|
||||
match (block_validity_window.start(), block_validity_window.end()) {
|
||||
(Some(s), Some(e)) => s <= block_id && block_id < e,
|
||||
(Some(s), None) => s <= block_id,
|
||||
(None, Some(e)) => block_id < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case::test_case((Some(1), Some(3)), 3; "at upper bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 2; "inside range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 0; "below range")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 1; "at lower bound")]
|
||||
#[test_case::test_case((Some(1), Some(3)), 4; "above range")]
|
||||
#[test_case::test_case((Some(1), None), 1; "lower bound only - at bound")]
|
||||
#[test_case::test_case((Some(1), None), 10; "lower bound only - above")]
|
||||
#[test_case::test_case((Some(1), None), 0; "lower bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 3; "upper bound only - at bound")]
|
||||
#[test_case::test_case((None, Some(3)), 0; "upper bound only - below")]
|
||||
#[test_case::test_case((None, Some(3)), 4; "upper bound only - above")]
|
||||
#[test_case::test_case((None, None), 0; "no bounds - always valid")]
|
||||
#[test_case::test_case((None, None), 100; "no bounds - always valid 2")]
|
||||
fn timestamp_validity_window_works_in_privacy_preserving_transactions(
|
||||
validity_window: (Option<Timestamp>, Option<Timestamp>),
|
||||
timestamp: Timestamp,
|
||||
) {
|
||||
let timestamp_validity_window: TimestampValidityWindow = validity_window.try_into().unwrap();
|
||||
let validity_window_program = crate::test_methods::validity_window();
|
||||
let account_keys = test_private_account_keys_1();
|
||||
let pre = AccountWithMetadata::new(Account::default(), false, (&account_keys.npk(), 0));
|
||||
let mut state = V03State::new().with_test_programs();
|
||||
let tx = {
|
||||
let (shared_secret, epk) =
|
||||
SharedSecretKey::encapsulate_deterministic(&account_keys.vpk(), &[0_u8; 32], 0);
|
||||
|
||||
let instruction = (
|
||||
BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window,
|
||||
);
|
||||
let (output, proof) = crate::privacy_preserving_transaction::circuit::execute_and_prove(
|
||||
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,
|
||||
commitment_root: DUMMY_COMMITMENT_HASH,
|
||||
}],
|
||||
&validity_window_program.into(),
|
||||
)
|
||||
.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)
|
||||
};
|
||||
let result = state.transition_from_privacy_preserving_transaction(&tx, 1, timestamp);
|
||||
let is_inside_validity_window = match (
|
||||
timestamp_validity_window.start(),
|
||||
timestamp_validity_window.end(),
|
||||
) {
|
||||
(Some(s), Some(e)) => s <= timestamp && timestamp < e,
|
||||
(Some(s), None) => s <= timestamp,
|
||||
(None, Some(e)) => timestamp < e,
|
||||
(None, None) => true,
|
||||
};
|
||||
if is_inside_validity_window {
|
||||
assert!(result.is_ok());
|
||||
} else {
|
||||
assert!(matches!(result, Err(LeeError::OutOfValidityWindow)));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
521
lee/state_machine/src/validated_state_diff/mod.rs
Normal file
521
lee/state_machine/src/validated_state_diff/mod.rs
Normal file
@ -0,0 +1,521 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use lee_core::{
|
||||
BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, Timestamp,
|
||||
account::{Account, AccountId, AccountWithMetadata},
|
||||
program::{
|
||||
ChainedCall, Claim, DEFAULT_PROGRAM_ID, ProgramId, compute_public_authorized_pdas,
|
||||
validate_execution,
|
||||
},
|
||||
};
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
V03State, ensure,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
privacy_preserving_transaction::{
|
||||
PrivacyPreservingTransaction, circuit::Proof, message::Message,
|
||||
},
|
||||
program::Program,
|
||||
program_deployment_transaction::ProgramDeploymentTransaction,
|
||||
public_transaction::PublicTransaction,
|
||||
state::MAX_NUMBER_CHAINED_CALLS,
|
||||
};
|
||||
|
||||
pub struct StateDiff {
|
||||
pub signer_account_ids: Vec<AccountId>,
|
||||
pub public_diff: HashMap<AccountId, Account>,
|
||||
pub new_commitments: Vec<Commitment>,
|
||||
pub new_nullifiers: Vec<Nullifier>,
|
||||
pub program: Option<Program>,
|
||||
}
|
||||
|
||||
/// The validated output of executing or verifying a transaction, ready to be applied to the state.
|
||||
///
|
||||
/// Can only be constructed by the transaction validation functions inside this crate, ensuring the
|
||||
/// diff has been checked before any state mutation occurs.
|
||||
pub struct ValidatedStateDiff(StateDiff);
|
||||
|
||||
impl ValidatedStateDiff {
|
||||
pub fn from_public_transaction(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, LeeError> {
|
||||
let signer_account_ids = authenticate_public_transaction_signers(tx, state)?;
|
||||
let message = tx.message();
|
||||
|
||||
ensure!(
|
||||
!message.account_ids.is_empty(),
|
||||
LeeError::InvalidInput("Public transaction must have at least one account".into())
|
||||
);
|
||||
|
||||
// All account_ids must be different
|
||||
ensure!(
|
||||
message.account_ids.iter().collect::<HashSet<_>>().len() == message.account_ids.len(),
|
||||
LeeError::InvalidInput("Duplicate account_ids found in message".into(),)
|
||||
);
|
||||
|
||||
// Build pre_states for execution
|
||||
let input_pre_states: Vec<_> = message
|
||||
.account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut state_diff: HashMap<AccountId, Account> = HashMap::new();
|
||||
|
||||
let initial_call = ChainedCall {
|
||||
program_id: message.program_id,
|
||||
instruction_data: message.instruction_data.clone(),
|
||||
pre_states: input_pre_states,
|
||||
pda_seeds: vec![],
|
||||
};
|
||||
|
||||
let initial_caller_data = CallerData {
|
||||
program_id: None,
|
||||
authorized_accounts: signer_account_ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
let mut chained_calls =
|
||||
VecDeque::<(ChainedCall, CallerData)>::from_iter([(initial_call, initial_caller_data)]);
|
||||
let mut chain_calls_counter = 0;
|
||||
|
||||
while let Some((chained_call, caller_data)) = chained_calls.pop_front() {
|
||||
ensure!(
|
||||
chain_calls_counter <= MAX_NUMBER_CHAINED_CALLS,
|
||||
LeeError::MaxChainedCallsDepthExceeded
|
||||
);
|
||||
|
||||
// Check that the `program_id` corresponds to a deployed program
|
||||
let Some(program) = state.programs().get(&chained_call.program_id) else {
|
||||
return Err(LeeError::InvalidInput("Unknown program".into()));
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
|
||||
chained_call.program_id, chained_call.pre_states, chained_call.instruction_data
|
||||
);
|
||||
let mut program_output = program.execute(
|
||||
caller_data.program_id,
|
||||
&chained_call.pre_states,
|
||||
&chained_call.instruction_data,
|
||||
)?;
|
||||
debug!(
|
||||
"Program {:?} output: {:?}",
|
||||
chained_call.program_id, program_output
|
||||
);
|
||||
|
||||
let authorized_pdas =
|
||||
compute_public_authorized_pdas(caller_data.program_id, &chained_call.pda_seeds);
|
||||
|
||||
// Account is authorized if it is either in the caller's authorized accounts or in the
|
||||
// list of PDAs the caller has authorized.
|
||||
let is_authorized = |account_id: &AccountId| {
|
||||
authorized_pdas.contains(account_id)
|
||||
|| caller_data.authorized_accounts.contains(account_id)
|
||||
};
|
||||
|
||||
for pre in &program_output.pre_states {
|
||||
let account_id = pre.account_id;
|
||||
// Check that the program output pre_states coincide with the values in the public
|
||||
// state or with any modifications to those values during the chain of calls.
|
||||
let expected_pre = state_diff
|
||||
.get(&account_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| state.get_account_by_id(account_id));
|
||||
ensure!(
|
||||
pre.account == expected_pre,
|
||||
InvalidProgramBehaviorError::InconsistentAccountPreState {
|
||||
account_id,
|
||||
expected: Box::new(expected_pre),
|
||||
actual: Box::new(pre.account.clone())
|
||||
}
|
||||
);
|
||||
|
||||
// Check that the program output pre_states marked as authorized are indeed
|
||||
// authorized, and vice-versa.
|
||||
let is_indeed_authorized = is_authorized(&account_id);
|
||||
ensure!(
|
||||
!pre.is_authorized || is_indeed_authorized,
|
||||
InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id }
|
||||
);
|
||||
ensure!(
|
||||
pre.is_authorized || !is_indeed_authorized,
|
||||
InvalidProgramBehaviorError::AuthorizedAccountMarkedAsNotAuthorized {
|
||||
account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that the program output's self_program_id matches the expected program ID.
|
||||
ensure!(
|
||||
program_output.self_program_id == chained_call.program_id,
|
||||
InvalidProgramBehaviorError::MismatchedProgramId {
|
||||
expected: chained_call.program_id,
|
||||
actual: program_output.self_program_id
|
||||
}
|
||||
);
|
||||
|
||||
// Verify that the program output's caller_program_id matches the actual caller.
|
||||
ensure!(
|
||||
program_output.caller_program_id == caller_data.program_id,
|
||||
InvalidProgramBehaviorError::MismatchedCallerProgramId {
|
||||
expected: caller_data.program_id,
|
||||
actual: program_output.caller_program_id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify execution corresponds to a well-behaved program.
|
||||
// See the # Programs section for the definition of the `validate_execution` method.
|
||||
validate_execution(
|
||||
&program_output.pre_states,
|
||||
&program_output.post_states,
|
||||
chained_call.program_id,
|
||||
)
|
||||
.map_err(InvalidProgramBehaviorError::ExecutionValidationFailed)?;
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
program_output.block_validity_window.is_valid_for(block_id)
|
||||
&& program_output
|
||||
.timestamp_validity_window
|
||||
.is_valid_for(timestamp),
|
||||
LeeError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
for (i, post) in program_output.post_states.iter_mut().enumerate() {
|
||||
let Some(claim) = post.required_claim() else {
|
||||
continue;
|
||||
};
|
||||
let pre = &program_output.pre_states[i];
|
||||
let account_id = pre.account_id;
|
||||
|
||||
// The invoked program can only claim accounts with default program id.
|
||||
ensure!(
|
||||
post.account().program_owner == DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::ClaimedNonDefaultAccount { account_id }
|
||||
);
|
||||
|
||||
match claim {
|
||||
Claim::Authorized => {
|
||||
// The program can only claim accounts that were authorized by the signer.
|
||||
ensure!(
|
||||
pre.is_authorized,
|
||||
InvalidProgramBehaviorError::ClaimedUnauthorizedAccount { account_id }
|
||||
);
|
||||
}
|
||||
Claim::Pda(seed) => {
|
||||
// The program can only claim accounts that correspond to the PDAs it is
|
||||
// authorized to claim. The public-execution path only sees public
|
||||
// accounts, so the public-PDA derivation is the correct formula here.
|
||||
let pda = AccountId::for_public_pda(&chained_call.program_id, &seed);
|
||||
ensure!(
|
||||
account_id == pda,
|
||||
InvalidProgramBehaviorError::MismatchedPdaClaim {
|
||||
expected: pda,
|
||||
actual: account_id
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
post.account_mut().program_owner = chained_call.program_id;
|
||||
}
|
||||
|
||||
// Update the state diff
|
||||
for (pre, post) in program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.zip(program_output.post_states.iter())
|
||||
{
|
||||
state_diff.insert(pre.account_id, post.account().clone());
|
||||
}
|
||||
|
||||
// Source from `program_output.pre_states`, not `chained_call.pre_states`:
|
||||
// the loop above already gates program_output's `is_authorized` via the
|
||||
// `!pre.is_authorized || is_indeed_authorized` check, while `chained_call.
|
||||
// pre_states` is caller-controlled and can be forged (audit-issue 91).
|
||||
//
|
||||
// Union with the caller's authorized set so that authorization is monotonically
|
||||
// growing: once an account is authorized at any point in the chain it remains
|
||||
// authorized for all subsequent calls.
|
||||
let authorized_accounts: HashSet<_> = caller_data
|
||||
.authorized_accounts
|
||||
.into_iter()
|
||||
.chain(
|
||||
program_output
|
||||
.pre_states
|
||||
.iter()
|
||||
.filter(|pre| pre.is_authorized)
|
||||
.map(|pre| pre.account_id),
|
||||
)
|
||||
.collect();
|
||||
for new_call in program_output.chained_calls.into_iter().rev() {
|
||||
chained_calls.push_front((
|
||||
new_call,
|
||||
CallerData {
|
||||
program_id: Some(chained_call.program_id),
|
||||
authorized_accounts: authorized_accounts.clone(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
chain_calls_counter = chain_calls_counter
|
||||
.checked_add(1)
|
||||
.expect("we check the max depth at the beginning of the loop");
|
||||
}
|
||||
|
||||
// Check that all modified uninitialized accounts where claimed
|
||||
for (account_id, post) in state_diff.iter().filter_map(|(account_id, post)| {
|
||||
let pre = state.get_account_by_id(*account_id);
|
||||
if pre.program_owner != DEFAULT_PROGRAM_ID {
|
||||
return None;
|
||||
}
|
||||
if pre == *post {
|
||||
return None;
|
||||
}
|
||||
Some((*account_id, post))
|
||||
}) {
|
||||
ensure!(
|
||||
post.program_owner != DEFAULT_PROGRAM_ID,
|
||||
InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id }
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff: state_diff,
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
program: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_privacy_preserving_transaction(
|
||||
tx: &PrivacyPreservingTransaction,
|
||||
state: &V03State,
|
||||
block_id: BlockId,
|
||||
timestamp: Timestamp,
|
||||
) -> Result<Self, LeeError> {
|
||||
let message = &tx.message;
|
||||
let witness_set = &tx.witness_set;
|
||||
|
||||
// 1. Commitments or nullifiers are non empty
|
||||
ensure!(
|
||||
!message.new_commitments.is_empty() || !message.new_nullifiers.is_empty(),
|
||||
LeeError::InvalidInput(
|
||||
"Empty commitments and empty nullifiers found in message".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// 2. Check there are no duplicate account_ids in the public_account_ids list.
|
||||
ensure!(
|
||||
n_unique(&message.public_account_ids) == message.public_account_ids.len(),
|
||||
LeeError::InvalidInput("Duplicate account_ids found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate nullifiers in the new_nullifiers list
|
||||
ensure!(
|
||||
n_unique(
|
||||
&message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.map(|(n, _)| n)
|
||||
.collect::<Vec<_>>()
|
||||
) == message.new_nullifiers.len(),
|
||||
LeeError::InvalidInput("Duplicate nullifiers found in message".into())
|
||||
);
|
||||
|
||||
// Check there are no duplicate commitments in the new_commitments list
|
||||
ensure!(
|
||||
n_unique(&message.new_commitments) == message.new_commitments.len(),
|
||||
LeeError::InvalidInput("Duplicate commitments found in message".into())
|
||||
);
|
||||
|
||||
// 3. Nonce checks and Valid signatures
|
||||
// Check exactly one nonce is provided for each signature
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
LeeError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Check the signatures are valid
|
||||
ensure!(
|
||||
witness_set.signatures_are_valid_for(message),
|
||||
LeeError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
// Check nonces corresponds to the current nonces on the public state.
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
LeeError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
// Verify validity window
|
||||
ensure!(
|
||||
message.block_validity_window.is_valid_for(block_id)
|
||||
&& message.timestamp_validity_window.is_valid_for(timestamp),
|
||||
LeeError::OutOfValidityWindow
|
||||
);
|
||||
|
||||
// Build pre_states for proof verification
|
||||
let public_pre_states: Vec<_> = message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.map(|account_id| {
|
||||
AccountWithMetadata::new(
|
||||
state.get_account_by_id(*account_id),
|
||||
signer_account_ids.contains(account_id),
|
||||
*account_id,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 4. Proof verification
|
||||
check_privacy_preserving_circuit_proof_is_valid(
|
||||
&witness_set.proof,
|
||||
&public_pre_states,
|
||||
message,
|
||||
)?;
|
||||
|
||||
// 5. Commitment freshness
|
||||
state.check_commitments_are_new(&message.new_commitments)?;
|
||||
|
||||
// 6. Nullifier uniqueness
|
||||
state.check_nullifiers_are_valid(&message.new_nullifiers)?;
|
||||
|
||||
let public_diff = message
|
||||
.public_account_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(message.public_post_states.clone())
|
||||
.collect();
|
||||
let new_nullifiers = message
|
||||
.new_nullifiers
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(nullifier, _)| nullifier)
|
||||
.collect();
|
||||
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids,
|
||||
public_diff,
|
||||
new_commitments: message.new_commitments.clone(),
|
||||
new_nullifiers,
|
||||
program: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn from_program_deployment_transaction(
|
||||
tx: &ProgramDeploymentTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Self, LeeError> {
|
||||
// TODO: remove clone
|
||||
let program = Program::new(tx.message.bytecode.clone().into())?;
|
||||
if state.programs().contains_key(&program.id()) {
|
||||
return Err(LeeError::ProgramAlreadyExists);
|
||||
}
|
||||
Ok(Self(StateDiff {
|
||||
signer_account_ids: vec![],
|
||||
public_diff: HashMap::new(),
|
||||
new_commitments: vec![],
|
||||
new_nullifiers: vec![],
|
||||
program: Some(program),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the public account changes produced by this transaction.
|
||||
///
|
||||
/// Used by callers (e.g. the sequencer) to inspect the diff before committing it, for example
|
||||
/// to enforce that system accounts are not modified by user transactions.
|
||||
#[must_use]
|
||||
pub fn public_diff(&self) -> HashMap<AccountId, Account> {
|
||||
self.0.public_diff.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn into_state_diff(self) -> StateDiff {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CallerData {
|
||||
program_id: Option<ProgramId>,
|
||||
authorized_accounts: HashSet<AccountId>,
|
||||
}
|
||||
|
||||
fn authenticate_public_transaction_signers(
|
||||
tx: &PublicTransaction,
|
||||
state: &V03State,
|
||||
) -> Result<Vec<AccountId>, LeeError> {
|
||||
let message = tx.message();
|
||||
let witness_set = tx.witness_set();
|
||||
|
||||
ensure!(
|
||||
message.nonces.len() == witness_set.signatures_and_public_keys.len(),
|
||||
LeeError::InvalidInput(
|
||||
"Mismatch between number of nonces and signatures/public keys".into(),
|
||||
)
|
||||
);
|
||||
|
||||
ensure!(
|
||||
witness_set.is_valid_for(message),
|
||||
LeeError::InvalidInput("Invalid signature for given message and public key".into())
|
||||
);
|
||||
|
||||
let signer_account_ids = tx.signer_account_ids();
|
||||
for (account_id, nonce) in signer_account_ids.iter().zip(&message.nonces) {
|
||||
let current_nonce = state.get_account_by_id(*account_id).nonce;
|
||||
ensure!(
|
||||
current_nonce == *nonce,
|
||||
LeeError::InvalidInput("Nonce mismatch".into())
|
||||
);
|
||||
}
|
||||
|
||||
Ok(signer_account_ids)
|
||||
}
|
||||
|
||||
fn check_privacy_preserving_circuit_proof_is_valid(
|
||||
proof: &Proof,
|
||||
public_pre_states: &[AccountWithMetadata],
|
||||
message: &Message,
|
||||
) -> Result<(), LeeError> {
|
||||
let output = PrivacyPreservingCircuitOutput {
|
||||
public_pre_states: public_pre_states.to_vec(),
|
||||
public_post_states: message.public_post_states.clone(),
|
||||
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,
|
||||
timestamp_validity_window: message.timestamp_validity_window,
|
||||
};
|
||||
proof
|
||||
.is_valid_for(&output)
|
||||
.then_some(())
|
||||
.ok_or(LeeError::InvalidPrivacyPreservingProof)
|
||||
}
|
||||
|
||||
fn n_unique<T: Eq + Hash>(data: &[T]) -> usize {
|
||||
let set: HashSet<&T> = data.iter().collect();
|
||||
set.len()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
533
lee/state_machine/src/validated_state_diff/tests.rs
Normal file
533
lee/state_machine/src/validated_state_diff/tests.rs
Normal file
@ -0,0 +1,533 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use lee_core::account::{Account, AccountId, Nonce};
|
||||
|
||||
use crate::{
|
||||
PrivateKey, PublicKey, V03State,
|
||||
error::{InvalidProgramBehaviorError, LeeError},
|
||||
program::Program,
|
||||
public_transaction::{Message, WitnessSet},
|
||||
validated_state_diff::ValidatedStateDiff,
|
||||
};
|
||||
|
||||
fn public_state_from_balances(initial_data: &[(AccountId, u128)]) -> HashMap<AccountId, Account> {
|
||||
initial_data
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(account_id, balance)| {
|
||||
(
|
||||
account_id,
|
||||
Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance,
|
||||
..Account::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[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).
|
||||
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_public_accounts(public_state_from_balances(&[(from, 100)]))
|
||||
.with_programs(std::iter::once(
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
));
|
||||
let program_id = crate::test_methods::simple_balance_transfer().id();
|
||||
let message =
|
||||
Message::try_new(program_id, vec![from, to], vec![Nonce(0), Nonce(0)], 5_u128).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.
|
||||
///
|
||||
/// `execute_and_prove` succeeds because each inner receipt is individually valid and the
|
||||
/// outer circuit faithfully commits whatever the attacker's program output says, including
|
||||
/// `victim(is_authorized=true)`. The circuit has no access to chain state and cannot know
|
||||
/// the victim never signed.
|
||||
///
|
||||
/// The host-side validator is what catches the attack: it independently reconstructs
|
||||
/// `public_pre_states` from chain state using `signer_account_ids.contains(victim_id) = false`,
|
||||
/// so it expects `victim(is_authorized=false)`. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_public_victim() {
|
||||
use lee_core::{
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
state::{CommitmentSet, tests::test_private_account_keys_1},
|
||||
};
|
||||
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
// Attacker controls a private account.
|
||||
let attacker_keys = test_private_account_keys_1();
|
||||
let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0);
|
||||
let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk());
|
||||
|
||||
let victim_id = AccountId::new([20_u8; 32]);
|
||||
let recipient_id = AccountId::new([42_u8; 32]);
|
||||
let victim_balance = 5_000_u128;
|
||||
|
||||
// genesis sets program_owner = simple_balance_transfer_program.id() on all accounts.
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[
|
||||
(victim_id, victim_balance),
|
||||
(recipient_id, 0),
|
||||
]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Build attacker's private account and its local commitment tree.
|
||||
let attacker_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let attacker_commitment = Commitment::new(&attacker_id, &attacker_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&attacker_commitment));
|
||||
let membership_proof = commitment_set
|
||||
.get_proof_for(&attacker_commitment)
|
||||
.expect("attacker commitment must be in the set");
|
||||
|
||||
let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id);
|
||||
|
||||
let victim_account = state.get_account_by_id(victim_id);
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_account.balance,
|
||||
victim_account.nonce.0,
|
||||
victim_account.program_owner,
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
|
||||
let p2 = crate::test_methods::malicious_launderer();
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
// [0] attacker — first seen in P1's program_output.pre_states
|
||||
// [1] victim — first seen in simple_balance_transfer's program_output.pre_states
|
||||
// [2] recipient — first seen in simple_balance_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,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public, // victim
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: all inner receipts are valid.
|
||||
// The outer circuit commits victim(is_authorized=true) to its journal.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack privacy transaction should be rejected with InvalidPrivacyPreservingProof"
|
||||
);
|
||||
assert_eq!(state.get_account_by_id(victim_id).balance, victim_balance);
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Private-victim variant of the authorization-injection attack. The test passes when the
|
||||
/// attack is rejected and the recipient's balance remains zero.
|
||||
///
|
||||
/// After the circuit's Vacant branch accepts the injected `victim(is_authorized=true)`
|
||||
/// verbatim, the attacker must choose how to declare the victim in `account_identities`.
|
||||
/// There are two routes, both closed:
|
||||
///
|
||||
/// - **mask=1 (`PrivateAuthorizedUpdate`)**: the circuit derives `account_id =
|
||||
/// AccountId::for_regular_private_account(&npk_from(nsk), identifier)` and asserts it matches
|
||||
/// `pre_state.account_id`. Passing this check requires the victim's `nsk`, which the attacker
|
||||
/// does not have. `execute_and_prove` panics inside the ZKVM and no proof is produced.
|
||||
///
|
||||
/// - **mask=0 (`Public`)**: the circuit places the account in `public_pre_states` and
|
||||
/// `execute_and_prove` succeeds. The host-side validator then reconstructs `public_pre_states`
|
||||
/// from chain state; `state.get_account_by_id(victim_id)` returns the default account (balance=0)
|
||||
/// because the victim has no public state entry. The committed journal and the reconstructed
|
||||
/// expected output diverge, `receipt.verify` fails, and `from_privacy_preserving_transaction`
|
||||
/// returns an error before any state is applied. This test exercises this route.
|
||||
#[test]
|
||||
fn privacy_malicious_programs_cannot_drain_private_victim() {
|
||||
use lee_core::{
|
||||
Commitment, EncryptedAccountData, InputAccountIdentity, SharedSecretKey,
|
||||
account::{Account, AccountWithMetadata},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::{ProgramWithDependencies, execute_and_prove},
|
||||
message::Message,
|
||||
witness_set::WitnessSet,
|
||||
},
|
||||
state::{
|
||||
CommitmentSet,
|
||||
tests::{test_private_account_keys_1, test_private_account_keys_2},
|
||||
},
|
||||
};
|
||||
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
// Attacker controls a private account.
|
||||
let attacker_keys = test_private_account_keys_1();
|
||||
let attacker_id = AccountId::for_regular_private_account(&attacker_keys.npk(), 0);
|
||||
let (attacker_ssk, attacker_epk) = SharedSecretKey::encapsulate(&attacker_keys.vpk());
|
||||
|
||||
// Victim is a private account — not registered in public chain state.
|
||||
let victim_keys = test_private_account_keys_2();
|
||||
let victim_id = AccountId::for_regular_private_account(&victim_keys.npk(), 0);
|
||||
let victim_balance = 5_000_u128;
|
||||
|
||||
let recipient_id = AccountId::new([42_u8; 32]);
|
||||
|
||||
// Victim has no public state entry; only recipient is registered at genesis.
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[(recipient_id, 0)]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Build attacker's private account and its local commitment tree.
|
||||
let attacker_account = Account {
|
||||
program_owner: crate::test_methods::simple_balance_transfer().id(),
|
||||
balance: 100,
|
||||
..Account::default()
|
||||
};
|
||||
let attacker_commitment = Commitment::new(&attacker_id, &attacker_account);
|
||||
let mut commitment_set = CommitmentSet::with_capacity(1);
|
||||
commitment_set.extend(std::slice::from_ref(&attacker_commitment));
|
||||
let membership_proof = commitment_set
|
||||
.get_proof_for(&attacker_commitment)
|
||||
.expect("attacker commitment must be in the set");
|
||||
|
||||
let attacker_pre = AccountWithMetadata::new(attacker_account, true, attacker_id);
|
||||
|
||||
// The attacker supplies the victim's account data directly — it cannot be read from
|
||||
// public state. The injected balance and program_owner allow simple_balance_transfer
|
||||
// to succeed inside the circuit, which has no access to chain state and cannot detect
|
||||
// that these values are fabricated.
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_balance,
|
||||
0_u128, // nonce
|
||||
crate::test_methods::simple_balance_transfer().id(), // program_owner
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
let instruction_data = Program::serialize_instruction(instruction).unwrap();
|
||||
|
||||
let p2 = crate::test_methods::malicious_launderer();
|
||||
let at = crate::test_methods::simple_balance_transfer();
|
||||
let program_with_deps = ProgramWithDependencies::new(
|
||||
crate::test_methods::malicious_injector(),
|
||||
[(p2.id(), p2), (at.id(), at)].into(),
|
||||
);
|
||||
|
||||
// account_identities order must match self.pre_states as built by the circuit:
|
||||
// [0] attacker — first seen in P1's program_output.pre_states
|
||||
// [1] victim — first seen in simple_balance_transfer's program_output.pre_states
|
||||
// [2] recipient — first seen in simple_balance_transfer's program_output.pre_states
|
||||
//
|
||||
// Victim is marked Public: the attacker has no nsk for the victim's private account,
|
||||
// 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,
|
||||
identifier: 0,
|
||||
},
|
||||
InputAccountIdentity::Public, // victim — attacker lacks victim's nsk
|
||||
InputAccountIdentity::Public, // recipient
|
||||
];
|
||||
|
||||
// execute_and_prove succeeds: simple_balance_transfer runs against the injected
|
||||
// victim(balance=5000, is_authorized=true) and produces valid inner receipts.
|
||||
// The outer circuit commits victim(is_authorized=true) to public_pre_states.
|
||||
let (circuit_output, proof) = execute_and_prove(
|
||||
vec![attacker_pre],
|
||||
instruction_data,
|
||||
account_identities,
|
||||
&program_with_deps,
|
||||
)
|
||||
.expect("execute_and_prove should succeed \u{2014} the programs execute correctly");
|
||||
|
||||
// public_account_ids lists the Public entries from account_identities, in order.
|
||||
// The single ciphertext belongs to attacker's private account update.
|
||||
let message = Message::try_from_circuit_output(
|
||||
vec![victim_id, recipient_id],
|
||||
vec![], // no public signers, no nonces
|
||||
circuit_output,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, proof, &[]); // no signatures
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(LeeError::InvalidPrivacyPreservingProof)),
|
||||
"attack on private victim should be rejected with InvalidPrivacyPreservingProof"
|
||||
);
|
||||
// Victim has no public balance to check; confirming the recipient received nothing
|
||||
// is sufficient to show no funds moved.
|
||||
assert_eq!(state.get_account_by_id(recipient_id).balance, 0);
|
||||
}
|
||||
|
||||
/// Two malicious programs (injector + launderer) attempt to drain a victim's balance
|
||||
/// without the victim signing anything. The test passes when the attack is rejected
|
||||
/// and the victim's balance is left untouched.
|
||||
///
|
||||
/// Attack flow:
|
||||
/// Transaction (attacker signs) → P1 (`malicious_injector`)
|
||||
/// → injects `victim(is_authorized=true)` into chained-call `pre_states` for P2
|
||||
/// P2 (`malicious_launderer`)
|
||||
/// → outputs empty pre/post states, forwarding the forged flag to `simple_balance_transfer`
|
||||
/// → if `authorized_accounts` were built from the injected `pre_states`,
|
||||
/// `{victim}.contains(victim)` would pass and the transfer would execute.
|
||||
///
|
||||
/// The validator must reject this: `authorized_accounts` must be derived from the
|
||||
/// parent program's own validated `program_output.pre_states`, not from the chained-call
|
||||
/// input, so a forged `is_authorized=true` flag is never trusted.
|
||||
#[test]
|
||||
fn malicious_programs_cannot_drain_victim_without_signature() {
|
||||
// p2_id, simple_balance_transfer_id, victim_id_raw, victim_balance, victim_nonce,
|
||||
// victim_program_owner, recipient_id_raw, amount.
|
||||
// Primitives only — AccountId/Account cannot round-trip through instruction_data
|
||||
// via risc0_zkvm::serde (SerializeDisplay issue).
|
||||
type InjectorInstruction = (
|
||||
lee_core::program::ProgramId, // p2_id
|
||||
lee_core::program::ProgramId, // simple_balance_transfer_id
|
||||
[u8; 32], // victim_id_raw
|
||||
u128, // victim_balance
|
||||
u128, // victim_nonce
|
||||
lee_core::program::ProgramId, // victim_program_owner
|
||||
[u8; 32], // recipient_id_raw
|
||||
u128, // amount
|
||||
);
|
||||
|
||||
let attacker_key = PrivateKey::try_new([10; 32]).unwrap();
|
||||
let attacker_id = AccountId::from(&PublicKey::new_from_private_key(&attacker_key));
|
||||
|
||||
let victim_key = PrivateKey::try_new([20; 32]).unwrap();
|
||||
let victim_id = AccountId::from(&PublicKey::new_from_private_key(&victim_key));
|
||||
|
||||
let recipient_id = AccountId::new([42; 32]);
|
||||
|
||||
let victim_balance = 5_000_u128;
|
||||
let state = V03State::new()
|
||||
.with_public_accounts(public_state_from_balances(&[
|
||||
(attacker_id, 100),
|
||||
(victim_id, victim_balance),
|
||||
(recipient_id, 0),
|
||||
]))
|
||||
.with_programs([
|
||||
crate::test_methods::simple_balance_transfer(),
|
||||
crate::test_methods::malicious_injector(),
|
||||
crate::test_methods::malicious_launderer(),
|
||||
]);
|
||||
|
||||
// Read victim state from chain, exactly as the attacker would.
|
||||
let victim_account = state.get_account_by_id(victim_id);
|
||||
|
||||
let instruction: InjectorInstruction = (
|
||||
crate::test_methods::malicious_launderer().id(),
|
||||
crate::test_methods::simple_balance_transfer().id(),
|
||||
*victim_id.value(),
|
||||
victim_account.balance,
|
||||
victim_account.nonce.0,
|
||||
victim_account.program_owner,
|
||||
*recipient_id.value(),
|
||||
victim_balance,
|
||||
);
|
||||
|
||||
let message = Message::try_new(
|
||||
crate::test_methods::malicious_injector().id(),
|
||||
vec![attacker_id],
|
||||
vec![Nonce(0)],
|
||||
instruction,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let witness_set = WitnessSet::for_message(&message, &[&attacker_key]);
|
||||
let tx = crate::PublicTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_public_transaction(&tx, &state, 1, 0);
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
result,
|
||||
Err(LeeError::InvalidProgramBehavior(
|
||||
InvalidProgramBehaviorError::InvalidAccountAuthorization { account_id }
|
||||
)) if account_id == victim_id
|
||||
),
|
||||
"attack transaction should be rejected with InvalidAccountAuthorization for the victim"
|
||||
);
|
||||
|
||||
// Confirm the victim's balance is untouched.
|
||||
let victim_balance_after = state.get_account_by_id(victim_id).balance;
|
||||
let recipient_balance_after = state.get_account_by_id(recipient_id).balance;
|
||||
|
||||
assert_eq!(
|
||||
victim_balance_after, victim_balance,
|
||||
"victim balance should be unchanged"
|
||||
);
|
||||
assert_eq!(
|
||||
recipient_balance_after, 0,
|
||||
"recipient should receive nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test: a `PrivacyPreservingTransaction` carrying a structurally invalid
|
||||
/// proof must be rejected with a clean `Err`.
|
||||
#[test]
|
||||
fn privacy_garbage_proof_is_rejected() {
|
||||
use lee_core::{
|
||||
Commitment,
|
||||
account::Account,
|
||||
program::{BlockValidityWindow, TimestampValidityWindow},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PrivacyPreservingTransaction,
|
||||
privacy_preserving_transaction::{
|
||||
circuit::Proof, message::Message, witness_set::WitnessSet,
|
||||
},
|
||||
};
|
||||
|
||||
let state = V03State::new();
|
||||
|
||||
// Minimal message that passes every check up to proof verification: a single
|
||||
// commitment satisfies the non-empty requirement, no signers makes the
|
||||
// nonce/signature checks vacuously true, and unbounded validity windows are valid
|
||||
// for any block/timestamp.
|
||||
let account_id = AccountId::from(&PublicKey::new_from_private_key(
|
||||
&PrivateKey::try_new([1_u8; 32]).unwrap(),
|
||||
));
|
||||
let commitment = Commitment::new(&account_id, &Account::default());
|
||||
let message = Message {
|
||||
public_account_ids: vec![],
|
||||
nonces: vec![],
|
||||
public_post_states: vec![],
|
||||
encrypted_private_post_states: vec![],
|
||||
new_commitments: vec![commitment],
|
||||
new_nullifiers: vec![],
|
||||
block_validity_window: BlockValidityWindow::new_unbounded(),
|
||||
timestamp_validity_window: TimestampValidityWindow::new_unbounded(),
|
||||
};
|
||||
|
||||
// Garbage proof bytes: not a valid borsh-encoded `InnerReceipt`.
|
||||
let garbage_proof = Proof::from_inner(vec![0xff_u8; 64]);
|
||||
let witness_set = WitnessSet::for_message(&message, garbage_proof, &[]);
|
||||
let tx = PrivacyPreservingTransaction::new(message, witness_set);
|
||||
|
||||
let result = ValidatedStateDiff::from_privacy_preserving_transaction(&tx, &state, 1, 0);
|
||||
|
||||
match result {
|
||||
Err(LeeError::InvalidPrivacyPreservingProof) => {}
|
||||
Err(other) => panic!("expected InvalidPrivacyPreservingProof, got {other:?}"),
|
||||
Ok(_) => panic!("garbage proof was accepted instead of rejected"),
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user