diff --git a/Cargo.lock b/Cargo.lock index 13903085..11201834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9035,6 +9035,7 @@ dependencies = [ "futures", "hex", "humantime-serde", + "key_protocol", "lee", "lee_core", "log", diff --git a/lee/state_machine/core/src/commitment.rs b/lee/state_machine/core/src/commitment.rs index 7c81c12c..92085d7d 100644 --- a/lee/state_machine/core/src/commitment.rs +++ b/lee/state_machine/core/src/commitment.rs @@ -52,6 +52,7 @@ impl std::fmt::Debug for Commitment { impl Commitment { /// Generates the commitment to a private account owned by user for `account_id`: /// SHA256( `Comm_DS` || `account_id` || `program_owner` || balance || nonce || SHA256(data)). + // TODO: Accept account_id by value as it's Copy #[must_use] pub fn new(account_id: &AccountId, account: &Account) -> Self { const COMMITMENT_PREFIX: &[u8; 32] = diff --git a/lee/state_machine/core/src/nullifier.rs b/lee/state_machine/core/src/nullifier.rs index d1fbae42..0490ac00 100644 --- a/lee/state_machine/core/src/nullifier.rs +++ b/lee/state_machine/core/src/nullifier.rs @@ -97,6 +97,7 @@ impl Nullifier { } /// Computes a nullifier for an account initialization. + // TODO: Accept account_id by value as it's Copy #[must_use] pub fn for_account_initialization(account_id: &AccountId) -> Self { const INIT_PREFIX: &[u8; 32] = b"/LEE/v0.3/Nullifier/Initialize/\x00"; diff --git a/lee/state_machine/src/state.rs b/lee/state_machine/src/state.rs index 4b74cf55..d0b3cb78 100644 --- a/lee/state_machine/src/state.rs +++ b/lee/state_machine/src/state.rs @@ -4725,4 +4725,90 @@ pub mod tests { assert_eq!(state.get_account_by_id(recipient_id).balance, amount); } + + #[test] + fn private_bridge_withdraw_invocation_is_dropped() { + let sender_keys = test_private_account_keys_1(); + let sender_private_account = Account { + // Keep sender private account owned by a non-authenticated-transfer program + // so bridge::Withdraw is rejected. + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + data: Data::default(), + }; + + let mut state = V03State::new_with_genesis_accounts(&[], vec![], 0) + .with_private_account(&sender_keys, &sender_private_account); + + 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 bridge_account_id = system_bridge_account_id(); + + let sender_pre = + AccountWithMetadata::new(sender_private_account, true, (&sender_keys.npk(), 0)); + let bridge_pre = AccountWithMetadata::new( + state.get_account_by_id(bridge_account_id), + false, + bridge_account_id, + ); + + let esk = [3; 32]; + let shared_secret = SharedSecretKey::new(esk, &sender_keys.vpk()); + + let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { + amount: 1, + bedrock_account_pk: [0; 32], + }) + .unwrap(); + + let program_with_deps = ProgramWithDependencies::new( + Program::bridge(), + [( + Program::authenticated_transfer_program().id(), + Program::authenticated_transfer_program(), + )] + .into(), + ); + + let bridge_nonce = bridge_pre.account.nonce; + + let (output, proof) = execute_and_prove( + vec![sender_pre, bridge_pre], + instruction, + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.nsk, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .expect("Execution should succeed"); + + let message = Message::try_from_circuit_output( + vec![bridge_account_id], + vec![bridge_nonce], + vec![( + sender_keys.npk(), + sender_keys.vpk(), + EphemeralPublicKey::from_scalar(esk), + )], + output, + ) + .expect("Message construction should succeed"); + let witness_set = WitnessSet::for_message(&message, proof, &[]); + let tx = PrivacyPreservingTransaction::new(message, witness_set); + let res = state.transition_from_privacy_preserving_transaction(&tx, 1, 0); + + assert!( + res.is_err(), + "Bridge withdraw invocation should be rejected in private execution" + ); + } } diff --git a/lez/sequencer/core/Cargo.toml b/lez/sequencer/core/Cargo.toml index d5bd5c3a..8a6a8c08 100644 --- a/lez/sequencer/core/Cargo.toml +++ b/lez/sequencer/core/Cargo.toml @@ -47,3 +47,4 @@ mock = [] futures.workspace = true test_program_methods.workspace = true lee = { workspace = true, features = ["test-utils"] } +key_protocol.workspace = true diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index b7321ba5..35e27a10 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -757,6 +757,20 @@ mod tests { test_utils::sequencer_sign_key_for_testing, transaction::{LeeTransaction, clock_invocation}, }; + use key_protocol::key_management::KeyChain; + use lee::{ + Account, AccountId, Data, EphemeralPublicKey, PrivacyPreservingTransaction, + SharedSecretKey, V03State, + error::LeeError, + execute_and_prove, + privacy_preserving_transaction::{Message, circuit::ProgramWithDependencies}, + program::Program, + system_bridge_account_id, + }; + use lee_core::{ + Commitment, InputAccountIdentity, Nullifier, + account::{AccountWithMetadata, Nonce}, + }; use logos_blockchain_core::mantle::ops::channel::ChannelId; use mempool::MemPoolHandle; use storage::sequencer::sequencer_cells::PendingDepositEventRecord; @@ -1531,4 +1545,99 @@ mod tests { "Block production should abort when clock account data is corrupted" ); } + + #[test] + fn private_bridge_withdraw_invocation_is_dropped() { + let sender_keys = KeyChain::new_os_random(); + let sender_account_id = + AccountId::for_regular_private_account(&sender_keys.nullifier_public_key, 0); + let sender_private_account = Account { + program_owner: Program::authenticated_transfer_program().id(), + balance: 100, + nonce: Nonce(0xdead_beef), + data: Data::default(), + }; + + let mut state = V03State::new_with_genesis_accounts( + &[], + vec![( + Commitment::new(&sender_account_id, &sender_private_account), + Nullifier::for_account_initialization(&sender_account_id), + )], + 0, + ); + + let sender_commitment = Commitment::new(&sender_account_id, &sender_private_account); + let bridge_account_id = system_bridge_account_id(); + + let sender_pre = AccountWithMetadata::new( + sender_private_account, + true, + (&sender_keys.nullifier_public_key, 0), + ); + let bridge_pre = AccountWithMetadata::new( + state.get_account_by_id(bridge_account_id), + false, + bridge_account_id, + ); + + let shared_secret = SharedSecretKey::encapsulate(&sender_keys.viewing_public_key).0; + + let instruction = Program::serialize_instruction(bridge_core::Instruction::Withdraw { + amount: 1, + bedrock_account_pk: [0; 32], + }) + .unwrap(); + + let program_with_deps = ProgramWithDependencies::new( + Program::bridge(), + [( + Program::authenticated_transfer_program().id(), + Program::authenticated_transfer_program(), + )] + .into(), + ); + + let (output, proof) = execute_and_prove( + vec![sender_pre, bridge_pre], + instruction, + vec![ + InputAccountIdentity::PrivateAuthorizedUpdate { + ssk: shared_secret, + nsk: sender_keys.private_key_holder.nullifier_secret_key, + membership_proof: state + .get_proof_for_commitment(&sender_commitment) + .expect("sender commitment must be in state"), + identifier: 0, + }, + InputAccountIdentity::Public, + ], + &program_with_deps, + ) + .expect("Execution should succeed"); + + let message = Message::try_from_circuit_output( + vec![bridge_account_id], + vec![], + vec![( + sender_keys.nullifier_public_key, + sender_keys.viewing_public_key, + EphemeralPublicKey(vec![12_u8; 1088]), + )], + output, + ) + .expect("Message construction should succeed"); + let witness_set = + lee::privacy_preserving_transaction::WitnessSet::for_message(&message, proof, &[]); + let tx = LeeTransaction::PrivacyPreserving(PrivacyPreservingTransaction::new( + message, + witness_set, + )); + let res = tx.execute_check_on_state(&mut state, 1, 0); + + assert!( + matches!(res, Err(LeeError::InvalidInput(_))), + "Bridge withdraw invocation should be rejected in private execution" + ); + } }