diff --git a/lee/privacy_preserving_circuit/src/execution_state.rs b/lee/privacy_preserving_circuit/src/execution_state.rs index bb6da5dab..deb995cf7 100644 --- a/lee/privacy_preserving_circuit/src/execution_state.rs +++ b/lee/privacy_preserving_circuit/src/execution_state.rs @@ -5,7 +5,7 @@ use std::{ use lee_core::{ Identifier, InputAccountIdentity, NullifierPublicKey, PrivateWitness, WitnessKind, - account::{Account, AccountId, AccountWithMetadata}, + account::{Account, AccountId, AccountWithMetadata, Data}, encryption::ViewingPublicKey, program::{ AccountPostState, BlockValidityWindow, CallerData, ChainedCall, Claim, @@ -249,8 +249,8 @@ impl ExecutionState { .filter(|(pre_default, post)| pre_default.account != **post) .map(|(pre, post)| (pre.account_id, post)) { - assert_ne!( - post.program_owner, DEFAULT_PROGRAM_OWNER, + assert!( + post.program_owner != DEFAULT_PROGRAM_OWNER || post.data == Data::default(), "Account {account_id} was modified but not claimed" ); } diff --git a/lee/state_machine/core/src/program/mod.rs b/lee/state_machine/core/src/program/mod.rs index 182d4e9a0..ced27f820 100644 --- a/lee/state_machine/core/src/program/mod.rs +++ b/lee/state_machine/core/src/program/mod.rs @@ -658,9 +658,9 @@ pub enum ExecutionValidationError { }, #[error( - "Post-state for account {account_id} has default program owner but pre-state was not default" + "Post-state for account {account_id} has default program owner but pre-state carries data" )] - NonDefaultAccountWithDefaultOwner { account_id: AccountId }, + DataBearingAccountWithDefaultOwner { account_id: AccountId }, #[error("Total balance across accounts overflowed 2^256 - 1")] BalanceSumOverflow, @@ -806,12 +806,12 @@ pub fn validate_execution( }); } - // 7. If a post state has default program owner, the pre state must have been a default - // account - if post.account.program_owner == DEFAULT_PROGRAM_OWNER && pre.account != Account::default() + // 7. A post state with default program owner must not have carried data in its pre state + if post.account.program_owner == DEFAULT_PROGRAM_OWNER + && pre.account.data != Data::default() { return Err( - ExecutionValidationError::NonDefaultAccountWithDefaultOwner { + ExecutionValidationError::DataBearingAccountWithDefaultOwner { account_id: pre.account_id, }, ); diff --git a/lee/state_machine/src/state/tests/public_program_rules.rs b/lee/state_machine/src/state/tests/public_program_rules.rs index a6d38b2ec..6c4cc8462 100644 --- a/lee/state_machine/src/state/tests/public_program_rules.rs +++ b/lee/state_machine/src/state/tests/public_program_rules.rs @@ -375,3 +375,112 @@ fn program_should_fail_if_does_not_preserve_total_balance_by_burning() { ))) if total_balance_pre_states == 100.into() && total_balance_post_states == 99.into() )); } + +#[test] +fn default_owner_credited_without_claim_succeeds() { + let sender_key = PrivateKey::try_new([1; 32]).unwrap(); + let sender = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let recipient = AccountId::new([9; 32]); + let transfer = crate::test_methods::modified_transfer_program(); + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account( + sender, + Account { + program_owner: transfer.id().into(), + balance: 500_000, + ..Account::default() + }, + ); + assert_eq!(state.get_account_by_id(recipient), Account::default()); + + let amount: u128 = 1; + let message = public_transaction::Message::try_new( + transfer.id(), + vec![sender, recipient], + vec![Nonce(0)], + amount, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + let tx = PublicTransaction::new(message, witness_set); + + state.transition_from_public_transaction(&tx, 1, 0).unwrap(); + + let credited = state.get_account_by_id(recipient); + assert_eq!(credited.program_owner, Account::default().program_owner); + assert_eq!(credited.data, Data::default()); + assert!(credited.balance > 0); +} + +#[test] +fn default_owner_balance_is_frozen_even_for_its_signer() { + let sender_key = PrivateKey::try_new([1; 32]).unwrap(); + let sender = AccountId::from(&PublicKey::new_from_private_key(&sender_key)); + let recipient = AccountId::new([9; 32]); + let transfer = crate::test_methods::modified_transfer_program(); + + let mut state = V03State::new().with_test_programs(); + state.force_insert_account( + sender, + Account { + balance: 500_000, + ..Account::default() + }, + ); + + let amount: u128 = 1; + let message = public_transaction::Message::try_new( + transfer.id(), + vec![sender, recipient], + vec![Nonce(0)], + amount, + ) + .unwrap(); + let witness_set = public_transaction::WitnessSet::for_message(&message, &[&sender_key]); + 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, owner_account_id, executing_program_id } + ))) if account_id == sender + && owner_account_id == Account::default().program_owner + && executing_program_id == transfer.id() + )); + assert_eq!(state.get_account_by_id(sender).balance, 500_000); +} + +#[test] +fn default_owner_left_with_data_still_rejected() { + let account_id = AccountId::new([1; 32]); + let mut state = V03State::new().with_test_programs(); + state.force_insert_account( + account_id, + Account { + data: vec![0xca, 0xfe].try_into().unwrap(), + ..Account::default() + }, + ); + + let program_id = crate::test_methods::noop().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::DataBearingAccountWithDefaultOwner { + account_id: err_account_id + } + ) + )) if err_account_id == account_id + )); +} diff --git a/lee/state_machine/src/validated_state_diff/mod.rs b/lee/state_machine/src/validated_state_diff/mod.rs index dfe1b26ae..4b35385fe 100644 --- a/lee/state_machine/src/validated_state_diff/mod.rs +++ b/lee/state_machine/src/validated_state_diff/mod.rs @@ -6,7 +6,7 @@ use std::{ use lee_core::{ BlockId, Commitment, Nullifier, PrivacyPreservingCircuitOutput, PublicAction, Timestamp, - account::{Account, AccountId, AccountWithMetadata}, + account::{Account, AccountId, AccountWithMetadata, Data}, program::{ CallerData, ChainedCall, Claim, DEFAULT_PROGRAM_ID, DEFAULT_PROGRAM_OWNER, SystemInstruction, compute_public_authorized_pdas, validate_clear, validate_execution, @@ -333,7 +333,7 @@ impl ValidatedStateDiff { Some((*account_id, post)) }) { ensure!( - post.program_owner != DEFAULT_PROGRAM_OWNER, + post.program_owner != DEFAULT_PROGRAM_OWNER || post.data == Data::default(), InvalidProgramBehaviorError::DefaultAccountModifiedWithoutClaim { account_id } ); }