arbos/arbos_state/
initialize.rs

1use alloy_primitives::{Address, B256, U256};
2use arb_storage::{
3    ARBOS_STATE_ADDRESS, Storage, StorageBackedAddress, StorageBackedBigUint, StorageBackend,
4    set_account_nonce,
5};
6use revm::{Database, database::State};
7
8use super::{ArbosState, ArbosStateError};
9use crate::{
10    burn::Burner,
11    l1_pricing::L1PricingState,
12    l2_pricing::L2PricingState,
13    retryables::{self, RetryableState},
14};
15
16/// Genesis data for a retryable ticket.
17#[derive(Debug, Clone)]
18pub struct InitRetryableData {
19    pub id: B256,
20    pub timeout: u64,
21    pub from: Address,
22    pub to: Option<Address>,
23    pub callvalue: U256,
24    pub beneficiary: Address,
25    pub calldata: Vec<u8>,
26}
27
28/// Genesis data for an account.
29#[derive(Debug, Clone)]
30pub struct AccountInitInfo {
31    pub addr: Address,
32    pub nonce: u64,
33    pub balance: U256,
34    pub contract_info: Option<ContractInitInfo>,
35    pub aggregator_info: Option<AggregatorInitInfo>,
36}
37
38/// Contract info for genesis account initialization.
39#[derive(Debug, Clone)]
40pub struct ContractInitInfo {
41    pub code: Vec<u8>,
42    pub storage: Vec<(U256, U256)>,
43}
44
45/// Aggregator (batch poster) info for genesis account initialization.
46#[derive(Debug, Clone)]
47pub struct AggregatorInitInfo {
48    pub fee_collector: Address,
49}
50
51/// Creates a genesis block header.
52///
53/// Returns the fields needed for the genesis block. The actual block
54/// construction uses reth's block types, so this returns a struct
55/// that the genesis pipeline can consume.
56#[derive(Debug, Clone)]
57pub struct GenesisBlockInfo {
58    pub parent_hash: B256,
59    pub block_number: u64,
60    pub timestamp: u64,
61    pub state_root: B256,
62    pub gas_limit: u64,
63    pub base_fee: u64,
64    pub nonce: u64,
65    pub arbos_format_version: u64,
66}
67
68/// Build genesis block info from chain parameters.
69pub fn make_genesis_block(
70    parent_hash: B256,
71    block_number: u64,
72    timestamp: u64,
73    state_root: B256,
74    initial_arbos_version: u64,
75) -> GenesisBlockInfo {
76    use crate::l2_pricing;
77
78    GenesisBlockInfo {
79        parent_hash,
80        block_number,
81        timestamp,
82        state_root,
83        gas_limit: l2_pricing::GETH_BLOCK_GAS_LIMIT,
84        base_fee: l2_pricing::INITIAL_BASE_FEE_WEI,
85        nonce: 1, // genesis reads the init message
86        arbos_format_version: initial_arbos_version,
87    }
88}
89
90/// Initialize retryable tickets from genesis data.
91///
92/// Expired retryables (timeout <= current_timestamp) are skipped, and their
93/// call value is returned as `(beneficiary, callvalue)` pairs for the caller
94/// to credit balances. Active retryables are sorted by timeout and created.
95///
96/// Returns `(balance_credits, escrow_credits)` where:
97/// - `balance_credits`: expired retryable beneficiaries to credit
98/// - `escrow_credits`: (escrow_address, callvalue) for active retryable escrow funding
99pub fn initialize_retryables<D: Database, C: StorageBackend>(
100    backend: &mut C,
101    rs: &RetryableState<D>,
102    mut retryables_data: Vec<InitRetryableData>,
103    current_timestamp: u64,
104) -> Result<(Vec<(Address, U256)>, Vec<(Address, U256)>), ArbosStateError> {
105    let mut balance_credits = Vec::new();
106    let mut active_retryables = Vec::new();
107
108    for r in retryables_data.drain(..) {
109        if r.timeout <= current_timestamp {
110            balance_credits.push((r.beneficiary, r.callvalue));
111            continue;
112        }
113        active_retryables.push(r);
114    }
115
116    active_retryables.sort_by(|a, b| a.timeout.cmp(&b.timeout).then_with(|| a.id.cmp(&b.id)));
117
118    let mut escrow_credits = Vec::new();
119
120    for r in &active_retryables {
121        let escrow_addr = retryables::retryable_escrow_address(r.id);
122        escrow_credits.push((escrow_addr, r.callvalue));
123        rs.create_retryable(
124            backend,
125            r.id,
126            r.timeout,
127            r.from,
128            r.to,
129            r.callvalue,
130            r.beneficiary,
131            &r.calldata,
132        )?;
133    }
134
135    Ok((balance_credits, escrow_credits))
136}
137
138/// Initialize an account's ArbOS-specific state during genesis.
139///
140/// If the account has aggregator info and is a known batch poster,
141/// sets the batch poster's pay-to (fee collector) address.
142pub fn initialize_arbos_account<D: Database, B: Burner, C: StorageBackend>(
143    backend: &mut C,
144    arbos_state: &ArbosState<'_, D, B>,
145    account: &AccountInitInfo,
146) -> Result<(), ArbosStateError> {
147    if let Some(ref aggregator) = account.aggregator_info {
148        let poster_table = arbos_state.l1_pricing_state.batch_poster_table();
149        let is_poster = poster_table.contains_poster(backend, account.addr)?;
150        if is_poster {
151            let poster = poster_table.open_poster(backend, account.addr, false)?;
152            poster.set_pay_to(backend, aggregator.fee_collector)?;
153        }
154    }
155    Ok(())
156}
157
158/// Full database initialization for ArbOS genesis.
159///
160/// This is the high-level orchestrator that:
161/// 1. Initializes ArbOS state (version upgrades, precompile code)
162/// 2. Adds chain owner
163/// 3. Imports address table entries
164/// 4. Imports retryable tickets
165/// 5. Imports account state (balances, nonces, code, storage, batch poster config)
166///
167/// The caller provides the state database, init data, and handles commits.
168/// Balance credits from expired retryables and escrow funding are returned
169/// for the caller to execute against the state.
170#[derive(Debug)]
171pub struct GenesisInitResult {
172    /// Expired retryable beneficiaries to credit.
173    pub balance_credits: Vec<(Address, U256)>,
174    /// Escrow addresses to fund for active retryables.
175    pub escrow_credits: Vec<(Address, U256)>,
176    /// Accounts to initialize (balances, nonces, code, storage).
177    pub accounts: Vec<AccountInitInfo>,
178}
179
180/// Initialize ArbOS in the database.
181///
182/// Creates the ArbOS state, adds the chain owner, imports address table
183/// entries, retryable tickets, and accounts. Returns a `GenesisInitResult`
184/// containing all balance operations the caller needs to execute.
185pub fn initialize_arbos_in_database<D: Database, B: Burner, C: StorageBackend>(
186    backend: &mut C,
187    arbos_state: &ArbosState<'_, D, B>,
188    chain_owner: Address,
189    address_table_entries: Vec<Address>,
190    retryable_data: Vec<InitRetryableData>,
191    accounts: Vec<AccountInitInfo>,
192    current_timestamp: u64,
193) -> Result<GenesisInitResult, ArbosStateError> {
194    if chain_owner != Address::ZERO {
195        arbos_state.chain_owners.add(backend, chain_owner)?;
196    }
197
198    let table_size = arbos_state.address_table.size(backend)?;
199    if table_size != 0 {
200        return Err(ArbosStateError::AddressTableNotEmpty);
201    }
202    for (i, addr) in address_table_entries.iter().enumerate() {
203        let (slot, _) = arbos_state.address_table.register(backend, *addr)?;
204        if slot != i as u64 {
205            return Err(ArbosStateError::AddressTableSlotMismatch);
206        }
207    }
208
209    let (balance_credits, escrow_credits) = initialize_retryables(
210        backend,
211        &arbos_state.retryable_state,
212        retryable_data,
213        current_timestamp,
214    )?;
215
216    for account in &accounts {
217        initialize_arbos_account(backend, arbos_state, account)?;
218    }
219
220    Ok(GenesisInitResult {
221        balance_credits,
222        escrow_credits,
223        accounts,
224    })
225}
226
227/// Bring a fresh database to a fully-initialised ArbOS state at the requested
228/// version, returning the opened state.
229pub fn bootstrap<'a, D: Database, B: Burner>(
230    state: &'a mut State<D>,
231    chain_id: u64,
232    network_fee_account: Address,
233    infra_fee_account: Address,
234    l1_initial_base_fee: U256,
235    target_arbos_version: u64,
236    burner: B,
237) -> Result<ArbosState<'a, D, B>, ArbosStateError> {
238    set_account_nonce(state, ARBOS_STATE_ADDRESS, 1);
239
240    {
241        let backing = Storage::<D>::new(state, B256::ZERO);
242        backing.set_by_uint64(super::VERSION_OFFSET, B256::from(U256::from(1u64)))?;
243        // SAFETY: see `Storage` struct-level invariant. The `&mut State`
244        // returned here is used transiently to drive `StorageBackend`-based
245        // setters and is dropped before any subsequent use of `backing`.
246        let s = unsafe { backing.state_mut() };
247        StorageBackedBigUint::new(B256::ZERO, super::CHAIN_ID_OFFSET)
248            .set(s, U256::from(chain_id))?;
249        // SAFETY: see above.
250        let s = unsafe { backing.state_mut() };
251        StorageBackedAddress::new(B256::ZERO, super::NETWORK_FEE_ACCOUNT_OFFSET)
252            .set(s, network_fee_account)?;
253        // SAFETY: see above.
254        let s = unsafe { backing.state_mut() };
255        StorageBackedAddress::new(B256::ZERO, super::INFRA_FEE_ACCOUNT_OFFSET)
256            .set(s, infra_fee_account)?;
257
258        let l1_sto = backing.open_sub_storage(super::L1_PRICING_SUBSPACE);
259        // SAFETY: see above.
260        let s = unsafe { backing.state_mut() };
261        L1PricingState::initialize(&l1_sto, s, network_fee_account, l1_initial_base_fee)?;
262        let l2_sto = backing.open_sub_storage(super::L2_PRICING_SUBSPACE);
263        // SAFETY: see above.
264        let s = unsafe { backing.state_mut() };
265        L2PricingState::<D>::initialize(&l2_sto, s)?;
266        RetryableState::<D>::initialize(&backing.open_sub_storage(super::RETRYABLES_SUBSPACE))?;
267    }
268
269    let mut arbos = ArbosState::open(state, burner)?;
270    // SAFETY: see `Storage` struct-level invariant.
271    let s = unsafe { arbos.backing_storage.state_mut() };
272    arbos.upgrade_arbos_version(s, target_arbos_version, true)?;
273    Ok(arbos)
274}