mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-07-17 13:20:17 +00:00
ValidatedStateDiff::from_public_transaction never checked that the accounts touched in a program's output matched the caller-declared message.account_ids — it just folded whatever pairs the program returned into the diff. A program (or a macro-generated dispatcher wrapping one) that silently drops an account from both sides of its own output together stays internally consistent (pre_states.len() == post_states.len()) and passes validate_execution's existing checks, so the dropped account simply vanishes with no error. Add a check after the chained-call loop: every account_id in message.account_ids must appear in the final state_diff, or the transaction is rejected with the new DeclaredAccountMissingFromOutput error. Add a dropped_account test-guest program that reproduces the exact shape of the bug (two pre_states in, one consistent (pre, post) pair out) and a regression test proving the transaction is now rejected. Verified the test fails with Ok(()) when the check is removed, and passes once it's restored.
378 lines
16 KiB
Rust
378 lines
16 KiB
Rust
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
|
|
));
|
|
}
|
|
|
|
/// A program can drop an entire account from its own output — both its `pre_state` and
|
|
/// `post_state` together, not just one side — while staying internally consistent
|
|
/// (`pre_states.len() == post_states.len()` within its own report, so `validate_execution`'s
|
|
/// length check alone can't catch it). This must still be rejected: every account the caller
|
|
/// declared in the transaction must appear somewhere in the final diff.
|
|
#[test]
|
|
fn program_should_fail_if_it_drops_a_declared_account() {
|
|
// Both accounts need a non-default program_owner: an account left at DEFAULT_PROGRAM_ID with
|
|
// non-default data would itself violate the (separate, pre-existing) "claim before mutating a
|
|
// default-owned account" rule the moment it's echoed back — unrelated to what this test
|
|
// targets. `with_public_account_balances` leaves program_owner at DEFAULT_PROGRAM_ID, so use
|
|
// `with_public_accounts` to set it explicitly instead.
|
|
let mut state = V03State::new()
|
|
.with_public_accounts([
|
|
(
|
|
AccountId::new([1; 32]),
|
|
Account {
|
|
program_owner: crate::test_methods::dropped_account().id(),
|
|
balance: 100,
|
|
..Account::default()
|
|
},
|
|
),
|
|
(
|
|
AccountId::new([2; 32]),
|
|
Account {
|
|
program_owner: crate::test_methods::dropped_account().id(),
|
|
balance: 0,
|
|
..Account::default()
|
|
},
|
|
),
|
|
])
|
|
.with_test_programs();
|
|
let account_ids = vec![AccountId::new([1; 32]), AccountId::new([2; 32])];
|
|
let program_id = crate::test_methods::dropped_account().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::DeclaredAccountMissingFromOutput { account_id }
|
|
)) if account_id == AccountId::new([2; 32])
|
|
),
|
|
"expected DeclaredAccountMissingFromOutput for the dropped account, got {result:?}"
|
|
);
|
|
}
|
|
|
|
#[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()
|
|
));
|
|
}
|