arbos/programs/
params.rs

1use arb_storage::{Storage, StorageBackend, SystemStateBackend};
2
3use super::ProgramsError;
4
5/// ArbOS version constants for feature gating.
6pub const ARBOS_VERSION_40: u64 = 40;
7pub const ARBOS_VERSION_50: u64 = 50;
8pub const ARBOS_VERSION_STYLUS_CONTRACT_LIMIT: u64 = 60;
9
10// Initial parameter values.
11const INITIAL_MAX_WASM_SIZE: u32 = 128 * 1024;
12const INITIAL_STACK_DEPTH: u32 = 4 * 65536;
13pub const INITIAL_FREE_PAGES: u16 = 2;
14pub const INITIAL_PAGE_GAS: u16 = 1000;
15pub const INITIAL_PAGE_RAMP: u64 = 620674314;
16const INITIAL_PAGE_LIMIT: u16 = 128;
17const INITIAL_INK_PRICE: u32 = 10000;
18pub const INITIAL_MAX_FRAGMENT_COUNT: u8 = 4;
19const INITIAL_MIN_INIT_GAS: u8 = 72;
20const INITIAL_MIN_CACHED_GAS: u8 = 11;
21const INITIAL_INIT_COST_SCALAR: u8 = 50;
22const INITIAL_CACHED_COST_SCALAR: u8 = 50;
23const INITIAL_EXPIRY_DAYS: u16 = 365;
24const INITIAL_KEEPALIVE_DAYS: u16 = 31;
25const INITIAL_RECENT_CACHE_SIZE: u16 = 32;
26
27const V2_MIN_INIT_GAS: u8 = 69;
28
29pub const MIN_CACHED_GAS_UNITS: u64 = 32;
30pub const MIN_INIT_GAS_UNITS: u64 = 128;
31pub const COST_SCALAR_PERCENT: u64 = 2;
32
33const ARBOS_50_MAX_STACK_DEPTH: u32 = 22000;
34const ARBOS_60_MAX_WASM_SIZE: u32 = 256 * 1024;
35
36/// Stylus configuration parameters packed into storage words.
37#[derive(Debug, Clone)]
38pub struct StylusParams {
39    pub arbos_version: u64,
40    pub version: u16,
41    pub ink_price: u32, // uint24 in Go, stored as u32
42    pub max_stack_depth: u32,
43    pub free_pages: u16,
44    pub page_gas: u16,
45    pub page_ramp: u64,
46    pub page_limit: u16,
47    pub min_init_gas: u8,
48    pub min_cached_init_gas: u8,
49    pub init_cost_scalar: u8,
50    pub cached_cost_scalar: u8,
51    pub expiry_days: u16,
52    pub keepalive_days: u16,
53    pub block_cache_size: u16,
54    pub max_wasm_size: u32,
55    pub max_fragment_count: u8,
56}
57
58impl StylusParams {
59    /// Deserialize params from a storage substorage through a [`SystemStateBackend`].
60    pub fn load<D, B: SystemStateBackend>(
61        arbos_version: u64,
62        sto: &Storage<'_, D>,
63        backend: &mut B,
64    ) -> Result<Self, ProgramsError> {
65        Self::load_from_reader(arbos_version, PackedReader::new(sto, backend))
66    }
67
68    fn load_from_reader<R: PackedRead>(
69        arbos_version: u64,
70        mut reader: R,
71    ) -> Result<Self, ProgramsError> {
72        let mut params = StylusParams {
73            arbos_version,
74            version: reader.take_u16()?,
75            ink_price: reader.take_u24()?,
76            max_stack_depth: reader.take_u32()?,
77            free_pages: reader.take_u16()?,
78            page_gas: reader.take_u16()?,
79            page_ramp: INITIAL_PAGE_RAMP,
80            page_limit: reader.take_u16()?,
81            min_init_gas: reader.take_u8()?,
82            min_cached_init_gas: reader.take_u8()?,
83            init_cost_scalar: reader.take_u8()?,
84            cached_cost_scalar: reader.take_u8()?,
85            expiry_days: reader.take_u16()?,
86            keepalive_days: reader.take_u16()?,
87            block_cache_size: reader.take_u16()?,
88            max_wasm_size: 0,
89            max_fragment_count: 0,
90        };
91
92        if arbos_version >= ARBOS_VERSION_40 {
93            params.max_wasm_size = reader.take_u32()?;
94        } else {
95            params.max_wasm_size = INITIAL_MAX_WASM_SIZE;
96        }
97        if arbos_version >= ARBOS_VERSION_STYLUS_CONTRACT_LIMIT {
98            params.max_fragment_count = reader.take_u8()?;
99        }
100
101        Ok(params)
102    }
103
104    /// Serialize and persist params through a [`StorageBackend`].
105    pub fn save<D, B: StorageBackend>(
106        &self,
107        sto: &Storage<'_, D>,
108        backend: &mut B,
109    ) -> Result<(), ProgramsError> {
110        use alloy_primitives::U256;
111        let data = self.encode();
112        let mut slot = 0u64;
113        let mut offset = 0;
114        while offset < data.len() {
115            let end = (offset + 32).min(data.len());
116            let chunk = &data[offset..end];
117
118            let mut word = [0u8; 32];
119            word[..chunk.len()].copy_from_slice(chunk);
120            let storage_slot = sto.new_slot(slot);
121            backend
122                .sstore(sto.account(), storage_slot, U256::from_be_bytes(word))
123                .map_err(Into::into)?;
124
125            slot += 1;
126            offset += 32;
127        }
128        Ok(())
129    }
130
131    fn encode(&self) -> Vec<u8> {
132        let mut data = Vec::with_capacity(32);
133
134        data.extend_from_slice(&self.version.to_be_bytes());
135        // uint24: 3 bytes big-endian
136        data.push((self.ink_price >> 16) as u8);
137        data.push((self.ink_price >> 8) as u8);
138        data.push(self.ink_price as u8);
139        data.extend_from_slice(&self.max_stack_depth.to_be_bytes());
140        data.extend_from_slice(&self.free_pages.to_be_bytes());
141        data.extend_from_slice(&self.page_gas.to_be_bytes());
142        data.extend_from_slice(&self.page_limit.to_be_bytes());
143        data.push(self.min_init_gas);
144        data.push(self.min_cached_init_gas);
145        data.push(self.init_cost_scalar);
146        data.push(self.cached_cost_scalar);
147        data.extend_from_slice(&self.expiry_days.to_be_bytes());
148        data.extend_from_slice(&self.keepalive_days.to_be_bytes());
149        data.extend_from_slice(&self.block_cache_size.to_be_bytes());
150
151        if self.arbos_version >= ARBOS_VERSION_40 {
152            data.extend_from_slice(&self.max_wasm_size.to_be_bytes());
153        }
154        if self.arbos_version >= ARBOS_VERSION_STYLUS_CONTRACT_LIMIT {
155            data.push(self.max_fragment_count);
156        }
157        data
158    }
159
160    /// Upgrade the params version (e.g. 1 -> 2 -> 3).
161    pub fn upgrade_to_version(&mut self, version: u16) -> Result<(), ProgramsError> {
162        match version {
163            2 => {
164                if self.version != 1 {
165                    return Err(ProgramsError::InvalidParamsUpgrade(
166                        "unexpected version for upgrade to 2",
167                    ));
168                }
169                self.version = 2;
170                self.min_init_gas = V2_MIN_INIT_GAS;
171                Ok(())
172            }
173            3 => {
174                if self.version != 2 {
175                    return Err(ProgramsError::InvalidParamsUpgrade(
176                        "unexpected version for upgrade to 3",
177                    ));
178                }
179                self.version = 3;
180                Ok(())
181            }
182            _ => Err(ProgramsError::InvalidParamsUpgrade(
183                "unsupported version upgrade",
184            )),
185        }
186    }
187
188    /// Handle ArbOS version upgrades that affect stylus params.
189    pub fn upgrade_to_arbos_version(
190        &mut self,
191        new_arbos_version: u64,
192    ) -> Result<(), ProgramsError> {
193        if self.arbos_version >= new_arbos_version {
194            return Err(ProgramsError::InvalidParamsUpgrade(
195                "unexpected arbos version downgrade",
196            ));
197        }
198
199        match new_arbos_version {
200            ARBOS_VERSION_50 => {
201                if self.max_stack_depth > ARBOS_50_MAX_STACK_DEPTH {
202                    self.max_stack_depth = ARBOS_50_MAX_STACK_DEPTH;
203                }
204            }
205            ARBOS_VERSION_40 => {
206                if self.version != 2 {
207                    return Err(ProgramsError::InvalidParamsUpgrade(
208                        "unexpected stylus version for arbos 40 upgrade",
209                    ));
210                }
211                self.max_wasm_size = INITIAL_MAX_WASM_SIZE;
212            }
213            ARBOS_VERSION_STYLUS_CONTRACT_LIMIT => {
214                self.max_wasm_size = ARBOS_60_MAX_WASM_SIZE;
215                self.max_fragment_count = INITIAL_MAX_FRAGMENT_COUNT;
216            }
217            _ => {}
218        }
219
220        self.arbos_version = new_arbos_version;
221        Ok(())
222    }
223}
224
225/// Initialize default stylus params and persist them via a [`StorageBackend`].
226pub fn init_stylus_params<D, B: StorageBackend>(
227    arbos_version: u64,
228    sto: &Storage<'_, D>,
229    backend: &mut B,
230) -> Result<(), ProgramsError> {
231    let mut params = StylusParams {
232        arbos_version,
233        version: 1,
234        ink_price: INITIAL_INK_PRICE,
235        max_stack_depth: INITIAL_STACK_DEPTH,
236        free_pages: INITIAL_FREE_PAGES,
237        page_gas: INITIAL_PAGE_GAS,
238        page_ramp: INITIAL_PAGE_RAMP,
239        page_limit: INITIAL_PAGE_LIMIT,
240        min_init_gas: INITIAL_MIN_INIT_GAS,
241        min_cached_init_gas: INITIAL_MIN_CACHED_GAS,
242        init_cost_scalar: INITIAL_INIT_COST_SCALAR,
243        cached_cost_scalar: INITIAL_CACHED_COST_SCALAR,
244        expiry_days: INITIAL_EXPIRY_DAYS,
245        keepalive_days: INITIAL_KEEPALIVE_DAYS,
246        block_cache_size: INITIAL_RECENT_CACHE_SIZE,
247        max_wasm_size: 0,
248        max_fragment_count: 0,
249    };
250    if arbos_version >= ARBOS_VERSION_STYLUS_CONTRACT_LIMIT {
251        params.max_wasm_size = ARBOS_60_MAX_WASM_SIZE;
252    } else if arbos_version >= ARBOS_VERSION_40 {
253        params.max_wasm_size = INITIAL_MAX_WASM_SIZE;
254    }
255    if arbos_version >= ARBOS_VERSION_STYLUS_CONTRACT_LIMIT {
256        params.max_fragment_count = INITIAL_MAX_FRAGMENT_COUNT;
257    }
258    params.save(sto, backend)
259}
260
261trait PackedRead {
262    fn take_u8(&mut self) -> Result<u8, ProgramsError>;
263    fn take_u16(&mut self) -> Result<u16, ProgramsError>;
264    fn take_u24(&mut self) -> Result<u32, ProgramsError>;
265    fn take_u32(&mut self) -> Result<u32, ProgramsError>;
266}
267
268struct PackedReader<'a, 'b, 'c, D, B: SystemStateBackend> {
269    sto: &'a Storage<'c, D>,
270    backend: &'b mut B,
271    slot: u64,
272    buf: [u8; 32],
273    pos: usize,
274}
275
276impl<'a, 'b, 'c, D, B: SystemStateBackend> PackedReader<'a, 'b, 'c, D, B> {
277    fn new(sto: &'a Storage<'c, D>, backend: &'b mut B) -> Self {
278        Self {
279            sto,
280            backend,
281            slot: 0,
282            buf: [0u8; 32],
283            pos: 32,
284        }
285    }
286
287    fn ensure(&mut self, count: usize) -> Result<(), ProgramsError> {
288        if self.pos + count > 32 {
289            let slot = self.sto.new_slot(self.slot);
290            let value = self
291                .backend
292                .sload_system(self.sto.account(), slot)
293                .map_err(Into::into)?;
294            self.buf = value.to_be_bytes::<32>();
295            self.slot += 1;
296            self.pos = 0;
297        }
298        Ok(())
299    }
300
301    fn take_bytes(&mut self, count: usize) -> Result<&[u8], ProgramsError> {
302        self.ensure(count)?;
303        let start = self.pos;
304        self.pos += count;
305        Ok(&self.buf[start..self.pos])
306    }
307}
308
309impl<D, B: SystemStateBackend> PackedRead for PackedReader<'_, '_, '_, D, B> {
310    fn take_u8(&mut self) -> Result<u8, ProgramsError> {
311        let bytes = self.take_bytes(1)?;
312        Ok(bytes[0])
313    }
314
315    fn take_u16(&mut self) -> Result<u16, ProgramsError> {
316        let bytes = self.take_bytes(2)?;
317        Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
318    }
319
320    fn take_u24(&mut self) -> Result<u32, ProgramsError> {
321        let bytes = self.take_bytes(3)?;
322        Ok((bytes[0] as u32) << 16 | (bytes[1] as u32) << 8 | bytes[2] as u32)
323    }
324
325    fn take_u32(&mut self) -> Result<u32, ProgramsError> {
326        let bytes = self.take_bytes(4)?;
327        Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
328    }
329}