diff --git a/lee/state_machine/core/src/program/mod.rs b/lee/state_machine/core/src/program/mod.rs index 752350fbc..70e9294b1 100644 --- a/lee/state_machine/core/src/program/mod.rs +++ b/lee/state_machine/core/src/program/mod.rs @@ -15,6 +15,25 @@ pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8]; /// TODO: Placeholder `program_owner` for uninitialized `Account`. pub const DEFAULT_PROGRAM_OWNER: AccountId = AccountId::new([0; 32]); +/// Sentinel `AccountId` that owns every deployed program's storage account. +/// +/// Now that program accounts live directly in `public_state` (Program-as-Account), a real +/// owner value matters: `DEFAULT_PROGRAM_OWNER` would leave them unclaimed, letting an +/// ordinary program invocation "claim" one via the normal claim path and then legitimately +/// rewrite its `data` (the elf) as if it were the program's own account — corrupting deployed +/// bytecode through completely ordinary dispatch. Self-ownership (`program_owner` equal to the +/// account's own id) is just as broken: it authorizes exactly the program whose invocation +/// would touch its own storage account. +/// +/// This value is reserved instead — no real `ProgramId`'s image id will ever equal it (it's a +/// fixed, distinctive bit pattern, not a hash, so collision with a real cryptographic digest is +/// as implausible as with `DEFAULT_PROGRAM_ID`), and it is a sentinel `Program::execute`s never +/// executes at, so no ordinary transaction can ever supply a `chained_call.program_id` whose +/// derived owner matches it. That makes every program's storage account unwritable via normal +/// dispatch, by construction, until a real loader program is designed and registered to hold +/// this authority deliberately. +pub const PROGRAM_STORAGE_OWNER: AccountId = AccountId::new([0xFF; 32]); + pub const MAX_NUMBER_CHAINED_CALLS: usize = 10; pub type ProgramId = [u32; 8]; diff --git a/lee/state_machine/src/state/mod.rs b/lee/state_machine/src/state/mod.rs index cb04e0e93..7c150260c 100644 --- a/lee/state_machine/src/state/mod.rs +++ b/lee/state_machine/src/state/mod.rs @@ -5,6 +5,7 @@ use lee_core::{ BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier, Timestamp, account::{Account, AccountId, Data}, + program::PROGRAM_STORAGE_OWNER, }; use crate::{ @@ -111,9 +112,12 @@ impl BorshDeserialize for NullifierSet { #[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[cfg_attr(test, derive(Debug))] pub struct V03State { + /// Deployed programs live here too, as `Account`s keyed by `AccountId::from(program_id)` + /// (see that impl's doc comment), with the elf held in `Account.data` and `program_owner` + /// set to the reserved `PROGRAM_STORAGE_OWNER` (see its doc comment for why that ownership + /// choice is load-bearing now that these accounts are reachable via ordinary dispatch). public_state: HashMap, private_state: (CommitmentSet, NullifierSet), - programs: HashMap, } impl Default for V03State { @@ -126,7 +130,6 @@ impl Default for V03State { Self { public_state: HashMap::default(), private_state, - programs: HashMap::default(), } } } @@ -197,11 +200,12 @@ impl V03State { pub(crate) fn insert_program(&mut self, program: &Program) { let account_id = AccountId::from(program.id()); let account = Account { + program_owner: PROGRAM_STORAGE_OWNER, data: Data::try_from(program.elf().to_vec()) .expect("elf must fit under DATA_MAX_LENGTH"), ..Account::default() }; - self.programs.insert(account_id, account); + self.public_state.insert(account_id, account); } pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) { @@ -286,17 +290,13 @@ impl V03State { self.private_state.0.get_proof_for(commitment) } - pub(crate) const fn programs(&self) -> &HashMap { - &self.programs - } - #[must_use] pub fn commitment_set_digest(&self) -> CommitmentSetDigest { self.private_state.0.digest() } - /// Order-independent fingerprint of the genesis-relevant state: the public - /// account set, the deployed program set, and the commitment-set digest. + /// Order-independent fingerprint of the genesis-relevant state: the public account set + /// (which includes deployed programs' storage accounts) and the commitment-set digest. /// /// The sequencer and the indexer build the directly-seeded part of genesis /// (base builtins plus any directly-seeded accounts) separately from their own @@ -313,18 +313,11 @@ impl V03State { let Self { public_state, private_state, - programs, } = self; let mut accounts: Vec<(&AccountId, &Account)> = public_state.iter().collect(); accounts.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref())); - - let mut program_accounts: Vec<(&AccountId, &Account)> = programs.iter().collect(); - program_accounts.sort_by(|a, b| a.0.as_ref().cmp(b.0.as_ref())); - let account_count = u64::try_from(accounts.len()).expect("account count fits in u64"); - let program_count = - u64::try_from(program_accounts.len()).expect("program count fits in u64"); let mut hasher = Sha256::new(); hasher.update(account_count.to_le_bytes()); @@ -335,14 +328,6 @@ impl V03State { hasher.update(len.to_le_bytes()); hasher.update(&bytes); } - hasher.update(program_count.to_le_bytes()); - for (id, account) in program_accounts { - hasher.update(id.as_ref()); - let bytes = borsh::to_vec(account).expect("Account is BorshSerialize"); - let len = u64::try_from(bytes.len()).expect("program account encoding fits in u64"); - hasher.update(len.to_le_bytes()); - hasher.update(&bytes); - } hasher.update(private_state.0.digest()); let mut out = [0_u8; 32]; diff --git a/lee/state_machine/src/state/tests/genesis.rs b/lee/state_machine/src/state/tests/genesis.rs index af118e15b..07b2e22c4 100644 --- a/lee/state_machine/src/state/tests/genesis.rs +++ b/lee/state_machine/src/state/tests/genesis.rs @@ -24,13 +24,10 @@ fn new_works() { ); 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] @@ -68,11 +65,11 @@ fn insert_program() { let program_to_insert = crate::test_methods::simple_balance_transfer(); let program_id = program_to_insert.id(); let account_id = lee_core::account::AccountId::from(program_id); - assert!(!state.programs.contains_key(&account_id)); + assert!(!state.public_state.contains_key(&account_id)); state.insert_program(&program_to_insert); - assert!(state.programs.contains_key(&account_id)); + assert!(state.public_state.contains_key(&account_id)); } #[test] @@ -106,15 +103,6 @@ fn get_account_by_account_id_default_account() { 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]); diff --git a/lee/state_machine/src/validated_state_diff/mod.rs b/lee/state_machine/src/validated_state_diff/mod.rs index cfbabab4e..82081ea5b 100644 --- a/lee/state_machine/src/validated_state_diff/mod.rs +++ b/lee/state_machine/src/validated_state_diff/mod.rs @@ -112,9 +112,13 @@ impl ValidatedStateDiff { LeeError::MaxChainedCallsDepthExceeded ); - let Some(program_account) = state - .programs() - .get(&AccountId::from(chained_call.program_id)) + // Check that the `program_id` corresponds to a deployed program. Deployed programs + // live in `public_state`, keyed by `AccountId::from(program_id)` (see that impl's + // doc comment), holding the elf as a plain `Account`; reconstruct a `Program` from + // it via `new_unchecked` to execute, skipping a redundant image-id recomputation + // since the id/elf pairing was already validated once, at deployment time. + let Some(program_account) = + state.get_account_by_id_ref(AccountId::from(chained_call.program_id)) else { return Err(LeeError::InvalidInput("Unknown program".into())); }; @@ -452,8 +456,8 @@ impl ValidatedStateDiff { // TODO: remove clone let program = Program::new(tx.message.bytecode.clone().into())?; if state - .programs() - .contains_key(&AccountId::from(program.id())) + .get_account_by_id_ref(AccountId::from(program.id())) + .is_some() { return Err(LeeError::ProgramAlreadyExists); }