refactor(lee): change programs shape (#720)

* feat(lee): store deployed programs as Account-shaped state, keyed by AccountId

Program-as-Account migration, first slice: V03State.programs becomes
HashMap<AccountId, Account> instead of HashMap<ProgramId, Program>,
with the elf held directly in Account.data. The map key is derived
from ProgramId via a new 1:1 From<ProgramId> for AccountId conversion
(both types are exactly 32 bytes) rather than a hash, since ProgramId
is already content-derived from the elf.

Account.program_owner stays ProgramId-typed everywhere - this only
changes how deployed programs are stored and looked up host-side, not
the dispatch/authorization model any guest program logic depends on.
Dispatch resolves a ChainedCall's program_id by converting to
AccountId, fetching the Account, and reconstructing a Program via
new_unchecked for execution.

DATA_MAX_LENGTH is raised from 100 KiB to 700 KiB to fit real program
elfs (observed 375 KB-631 KB) directly in Account.data; noted in its
docstring as a rough placeholder pending real transaction/block-size
budget analysis.

* fix(lee): store deployed programs as Account-shaped state, correct SeenShard cap

Corrects lee/state_machine internals for the Program-as-Account migration
and fixes SeenShard::MAX_DELIVERIES, which was still calibrated for the
old 100 KiB DATA_MAX_LENGTH instead of the current 700 KiB cap. Rebuilds
program artifacts and the sequencer test fixture to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* address PR #720 review nits

- Use FIXME instead of TODO for the temporary ProgramId->AccountId
  conversion, per review convention for patches guaranteed to be
  fixed later.
- Derive cross_zone_inbox's MAX_DELIVERIES from DATA_MAX_LENGTH
  instead of a hand-recomputed literal, so it stays in sync
  automatically the next time the cap changes.

* chore: regenerate artifacts after rebasing onto dev

Binary program artifacts and the prebuilt sequencer DB dump were left
as rebase-conflict placeholders; regenerated via `just build-artifacts`
against the fully rebased source.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jonesmarvin8
2026-08-18 10:06:42 -04:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 3a718c32f5
commit d52c76e2b5
26 changed files with 102 additions and 57 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -4,7 +4,9 @@ use borsh::{BorshDeserialize, BorshSerialize};
use bytesize::ByteSize;
use serde::{Deserialize, Serialize};
pub const DATA_MAX_LENGTH: ByteSize = ByteSize::kib(100);
/// TODO: Temporarily raised cap to 700 KiB from 100 KiB. This is a placeholder
/// until multiple accounts are used to store the entire elf.
pub const DATA_MAX_LENGTH: ByteSize = ByteSize::kib(700);
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, BorshSerialize)]
pub struct Data(Vec<u8>);
+13
View File
@@ -14,6 +14,19 @@ pub const DEFAULT_PROGRAM_ID: ProgramId = [0; 8];
pub const MAX_NUMBER_CHAINED_CALLS: usize = 10;
pub type ProgramId = [u32; 8];
/// FIXME: This is a temporary conversion; will be removed once `Program` to `Account`
/// migration is complete.
impl From<ProgramId> for AccountId {
fn from(program_id: ProgramId) -> Self {
let bytes: Vec<u8> = program_id
.iter()
.flat_map(|word| word.to_le_bytes())
.collect();
Self::new(bytes.try_into().expect("8 u32 words are exactly 32 bytes"))
}
}
pub type InstructionData = Vec<u32>;
pub struct ProgramInput<T> {
pub self_program_id: ProgramId,
+23 -15
View File
@@ -4,8 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
BlockId, Commitment, CommitmentSetDigest, DUMMY_COMMITMENT, MembershipProof, Nullifier,
Timestamp,
account::{Account, AccountId},
program::ProgramId,
account::{Account, AccountId, Data},
};
use crate::{
@@ -114,7 +113,7 @@ impl BorshDeserialize for NullifierSet {
pub struct V03State {
public_state: HashMap<AccountId, Account>,
private_state: (CommitmentSet, NullifierSet),
programs: HashMap<ProgramId, Program>,
programs: HashMap<AccountId, Account>,
}
impl Default for V03State {
@@ -190,13 +189,19 @@ impl V03State {
#[must_use]
pub fn with_programs(mut self, programs: impl IntoIterator<Item = Program>) -> Self {
for program in programs {
self.insert_program(program);
self.insert_program(&program);
}
self
}
pub(crate) fn insert_program(&mut self, program: Program) {
self.programs.insert(program.id(), program);
pub(crate) fn insert_program(&mut self, program: &Program) {
let account_id = AccountId::from(program.id());
let account = Account {
data: Data::try_from(program.elf().to_vec())
.expect("elf must fit under DATA_MAX_LENGTH"),
..Account::default()
};
self.programs.insert(account_id, account);
}
pub fn apply_state_diff(&mut self, diff: ValidatedStateDiff) {
@@ -222,7 +227,7 @@ impl V03State {
self.private_state.0.extend(&new_commitments);
self.private_state.1.extend(&new_nullifiers);
if let Some(program) = program {
self.insert_program(program);
self.insert_program(&program);
}
}
@@ -281,7 +286,7 @@ impl V03State {
self.private_state.0.get_proof_for(commitment)
}
pub(crate) const fn programs(&self) -> &HashMap<ProgramId, Program> {
pub(crate) const fn programs(&self) -> &HashMap<AccountId, Account> {
&self.programs
}
@@ -314,11 +319,12 @@ impl V03State {
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_ids: Vec<ProgramId> = programs.keys().copied().collect();
program_ids.sort_unstable();
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_ids.len()).expect("program 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());
@@ -330,10 +336,12 @@ impl V03State {
hasher.update(&bytes);
}
hasher.update(program_count.to_le_bytes());
for id in program_ids {
for word in id {
hasher.update(word.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());
+4 -3
View File
@@ -67,11 +67,12 @@ fn insert_program() {
let mut state = V03State::new();
let program_to_insert = crate::test_methods::simple_balance_transfer();
let program_id = program_to_insert.id();
assert!(!state.programs.contains_key(&program_id));
let account_id = lee_core::account::AccountId::from(program_id);
assert!(!state.programs.contains_key(&account_id));
state.insert_program(program_to_insert);
state.insert_program(&program_to_insert);
assert!(state.programs.contains_key(&program_id));
assert!(state.programs.contains_key(&account_id));
}
#[test]
+30 -30
View File
@@ -45,36 +45,36 @@ impl V03State {
/// Include test programs in the builtin programs map.
#[must_use]
pub fn with_test_programs(mut self) -> Self {
self.insert_program(crate::test_methods::simple_balance_transfer());
self.insert_program(crate::test_methods::nonce_changer());
self.insert_program(crate::test_methods::extra_output());
self.insert_program(crate::test_methods::missing_output());
self.insert_program(crate::test_methods::dropped_account());
self.insert_program(crate::test_methods::program_owner_changer());
self.insert_program(crate::test_methods::data_changer());
self.insert_program(crate::test_methods::minter());
self.insert_program(crate::test_methods::burner());
self.insert_program(crate::test_methods::auth_asserting_noop());
self.insert_program(crate::test_methods::private_pda_delegator());
self.insert_program(crate::test_methods::pda_claimer());
self.insert_program(crate::test_methods::two_pda_claimer());
self.insert_program(crate::test_methods::noop());
self.insert_program(crate::test_methods::chain_caller());
self.insert_program(crate::test_methods::modified_transfer_program());
self.insert_program(crate::test_methods::malicious_authorization_changer());
self.insert_program(crate::test_methods::validity_window());
self.insert_program(crate::test_methods::flash_swap_initiator());
self.insert_program(crate::test_methods::flash_swap_callback());
self.insert_program(crate::test_methods::malicious_self_program_id());
self.insert_program(crate::test_methods::malicious_caller_program_id());
self.insert_program(crate::test_methods::pda_spend_proxy());
self.insert_program(crate::test_methods::claimer());
self.insert_program(crate::test_methods::changer_claimer());
self.insert_program(crate::test_methods::validity_window_chain_caller());
self.insert_program(crate::test_methods::simple_transfer_proxy());
self.insert_program(crate::test_methods::malicious_injector());
self.insert_program(crate::test_methods::malicious_launderer());
self.insert_program(crate::test_methods::modified_transfer_program());
self.insert_program(&crate::test_methods::simple_balance_transfer());
self.insert_program(&crate::test_methods::nonce_changer());
self.insert_program(&crate::test_methods::extra_output());
self.insert_program(&crate::test_methods::missing_output());
self.insert_program(&crate::test_methods::dropped_account());
self.insert_program(&crate::test_methods::program_owner_changer());
self.insert_program(&crate::test_methods::data_changer());
self.insert_program(&crate::test_methods::minter());
self.insert_program(&crate::test_methods::burner());
self.insert_program(&crate::test_methods::auth_asserting_noop());
self.insert_program(&crate::test_methods::private_pda_delegator());
self.insert_program(&crate::test_methods::pda_claimer());
self.insert_program(&crate::test_methods::two_pda_claimer());
self.insert_program(&crate::test_methods::noop());
self.insert_program(&crate::test_methods::chain_caller());
self.insert_program(&crate::test_methods::modified_transfer_program());
self.insert_program(&crate::test_methods::malicious_authorization_changer());
self.insert_program(&crate::test_methods::validity_window());
self.insert_program(&crate::test_methods::flash_swap_initiator());
self.insert_program(&crate::test_methods::flash_swap_callback());
self.insert_program(&crate::test_methods::malicious_self_program_id());
self.insert_program(&crate::test_methods::malicious_caller_program_id());
self.insert_program(&crate::test_methods::pda_spend_proxy());
self.insert_program(&crate::test_methods::claimer());
self.insert_program(&crate::test_methods::changer_claimer());
self.insert_program(&crate::test_methods::validity_window_chain_caller());
self.insert_program(&crate::test_methods::simple_transfer_proxy());
self.insert_program(&crate::test_methods::malicious_injector());
self.insert_program(&crate::test_methods::malicious_launderer());
self.insert_program(&crate::test_methods::modified_transfer_program());
self
}
@@ -1,4 +1,5 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet, VecDeque},
hash::Hash,
};
@@ -111,10 +112,16 @@ impl ValidatedStateDiff {
LeeError::MaxChainedCallsDepthExceeded
);
// Check that the `program_id` corresponds to a deployed program
let Some(program) = state.programs().get(&chained_call.program_id) else {
let Some(program_account) = state
.programs()
.get(&AccountId::from(chained_call.program_id))
else {
return Err(LeeError::InvalidInput("Unknown program".into()));
};
let program = Program::new_unchecked(
chained_call.program_id,
Cow::Owned(program_account.data.to_vec()),
);
debug!(
"Program {:?} pre_states: {:?}, instruction_data: {:?}",
@@ -441,7 +448,10 @@ impl ValidatedStateDiff {
) -> Result<Self, LeeError> {
// TODO: remove clone
let program = Program::new(tx.message.bytecode.clone().into())?;
if state.programs().contains_key(&program.id()) {
if state
.programs()
.contains_key(&AccountId::from(program.id()))
{
return Err(LeeError::ProgramAlreadyExists);
}
Ok(Self(StateDiff {
+16 -5
View File
@@ -2,7 +2,7 @@ use std::collections::BTreeSet;
use borsh::{BorshDeserialize, BorshSerialize};
use lee_core::{
account::AccountId,
account::{AccountId, data::DATA_MAX_LENGTH},
program::{PdaSeed, ProgramId},
};
use serde::{Deserialize, Serialize};
@@ -133,13 +133,25 @@ impl SeenShard {
/// Deliveries one shard can hold before it exceeds `DATA_MAX_LENGTH`.
///
/// Borsh is 32 bytes of hash, a 4-byte count, then 4 bytes per index, so
/// this is exactly the 100 KiB an account may carry.
/// this is exactly the `DATA_MAX_LENGTH` an account may carry.
///
/// Out of reach only because of the L1 inscription cap: a block inscribes as
/// one op near 1.75 MiB and a minimal emitting transaction is about 257
/// bytes, capping a peer block near 7,100 deliveries. Raising that L1 cap
/// past roughly 6.3 MiB puts this back in reach.
pub const MAX_DELIVERIES: usize = 25_591;
pub const MAX_DELIVERIES: usize = {
let remaining_bytes = DATA_MAX_LENGTH.as_u64() - 36;
let count = remaining_bytes
.checked_div(4)
.expect("division is well-defined");
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "usize::try_from is not yet const-stable; the value is tiny and always fits"
)]
let count = count as usize;
count
};
/// Decodes a shard from account data; empty data is an unclaimed shard.
pub fn from_bytes(bytes: &[u8]) -> borsh::io::Result<Self> {
@@ -300,8 +312,6 @@ pub fn inbox_source_marker_seed(src_zone: &ZoneId, src_program_id: ProgramId) ->
}
#[cfg(test)]
mod tests {
use lee_core::account::data::DATA_MAX_LENGTH;
use super::*;
fn zone(b: u8) -> ZoneId {
@@ -379,6 +389,7 @@ mod tests {
#[test]
fn a_full_shard_fits_in_account_data() {
// Exact only because `DATA_MAX_LENGTH` is whole KiB, hence a multiple of 4.
let mut shard = SeenShard::default();
for index in 0..SeenShard::MAX_DELIVERIES {
shard.insert([5; 32], u32::try_from(index).expect("index fits"));
Binary file not shown.