feat!: admit data-free default owners

BREAKING

Before: Only owning programs can change data

After: Default-owned accounts can have their data wiped
This commit is contained in:
Artem Gureev
2026-08-24 21:58:02 +04:00
committed by agureev
parent 8d26a21990
commit bcb2862c55
4 changed files with 120 additions and 11 deletions
@@ -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"
);
}
+6 -6
View File
@@ -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,
},
);
@@ -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
));
}
@@ -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 }
);
}