arbos/programs/
api.rs

1use alloy_primitives::{Address, B256, U256};
2
3/// Offset applied to EVM API method request IDs in the Stylus protocol.
4pub const EVM_API_METHOD_REQ_OFFSET: u32 = 0x1000_0000;
5
6/// Status codes returned by host I/O operations to the Stylus runtime.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[repr(u8)]
9pub enum ApiStatus {
10    Success = 0,
11    Failure = 1,
12    OutOfGas = 2,
13    WriteProtection = 3,
14}
15
16impl ApiStatus {
17    pub fn from_u8(val: u8) -> Option<Self> {
18        match val {
19            0 => Some(Self::Success),
20            1 => Some(Self::Failure),
21            2 => Some(Self::OutOfGas),
22            3 => Some(Self::WriteProtection),
23            _ => None,
24        }
25    }
26}
27
28/// Host I/O operations available to Stylus WASM programs.
29///
30/// Each method corresponds to a `RequestType` variant and provides
31/// the bridge between the WASM runtime and the EVM state.
32pub trait HostIo {
33    type Error;
34
35    /// Read a storage slot.
36    fn get_bytes32(&mut self, key: B256) -> Result<(B256, u64), Self::Error>;
37
38    /// Write storage slots. Returns an API status and gas cost.
39    fn set_trie_slots(&mut self, data: &[u8], gas_left: &mut u64)
40        -> Result<ApiStatus, Self::Error>;
41
42    /// Read a transient storage slot.
43    fn get_transient_bytes32(&mut self, key: B256) -> Result<B256, Self::Error>;
44
45    /// Write a transient storage slot.
46    fn set_transient_bytes32(&mut self, key: B256, value: B256) -> Result<ApiStatus, Self::Error>;
47
48    /// Execute a CALL-family opcode.
49    fn contract_call(
50        &mut self,
51        contract: Address,
52        calldata: &[u8],
53        gas_left: u64,
54        gas_req: u64,
55        value: U256,
56        call_type: super::types::RequestType,
57    ) -> Result<(Vec<u8>, u64), Self::Error>;
58
59    /// Execute CREATE or CREATE2.
60    fn create(
61        &mut self,
62        code: &[u8],
63        endowment: U256,
64        salt: Option<U256>,
65        gas: u64,
66    ) -> Result<(Address, Vec<u8>, u64), Self::Error>;
67
68    /// Emit a log.
69    fn emit_log(&mut self, topics: &[B256], data: &[u8]) -> Result<(), Self::Error>;
70
71    /// Get an account's balance.
72    fn account_balance(&mut self, address: Address) -> Result<(U256, u64), Self::Error>;
73
74    /// Get an account's code.
75    fn account_code(&mut self, address: Address, gas: u64) -> Result<(Vec<u8>, u64), Self::Error>;
76
77    /// Get an account's code hash.
78    fn account_code_hash(&mut self, address: Address) -> Result<(B256, u64), Self::Error>;
79
80    /// Request additional WASM memory pages.
81    fn add_pages(&mut self, pages: u16) -> Result<u64, Self::Error>;
82
83    /// Capture host I/O for tracing.
84    fn capture_hostio(
85        &mut self,
86        name: &str,
87        args: &[u8],
88        outs: &[u8],
89        start_ink: u64,
90        end_ink: u64,
91    );
92}
93
94/// Dispatch a host I/O request to the appropriate HostIo method.
95///
96/// Takes a `RequestType` and input bytes and returns
97/// `(result, extra_data, gas_cost)`.
98pub fn dispatch_request<H: HostIo>(
99    host: &mut H,
100    req: super::types::RequestType,
101    input: &[u8],
102) -> Result<(Vec<u8>, Vec<u8>, u64), H::Error> {
103    use super::types::RequestType;
104
105    let mut parser = RequestParser::new(input);
106
107    match req {
108        RequestType::GetBytes32 => {
109            let key = parser.take_hash().expect("expected hash");
110            let (out, cost) = host.get_bytes32(key)?;
111            Ok((out.as_slice().to_vec(), Vec::new(), cost))
112        }
113        RequestType::SetTrieSlots => {
114            let gas_left = parser.take_u64().expect("expected u64");
115            let mut gas = gas_left;
116            let status = host.set_trie_slots(parser.take_rest(), &mut gas)?;
117            Ok((vec![status as u8], Vec::new(), gas_left - gas))
118        }
119        RequestType::GetTransientBytes32 => {
120            let key = parser.take_hash().expect("expected hash");
121            let out = host.get_transient_bytes32(key)?;
122            Ok((out.as_slice().to_vec(), Vec::new(), 0))
123        }
124        RequestType::SetTransientBytes32 => {
125            let key = parser.take_hash().expect("expected hash");
126            let value = parser.take_hash().expect("expected hash");
127            let status = host.set_transient_bytes32(key, value)?;
128            Ok((vec![status as u8], Vec::new(), 0))
129        }
130        RequestType::ContractCall | RequestType::DelegateCall | RequestType::StaticCall => {
131            let contract = parser.take_address().expect("expected address");
132            let value = parser.take_u256().expect("expected value");
133            let gas_left = parser.take_u64().expect("expected gas");
134            let gas_req = parser.take_u64().expect("expected gas req");
135            let calldata = parser.take_rest();
136
137            let (ret, cost) =
138                host.contract_call(contract, calldata, gas_left, gas_req, value, req)?;
139            let status: u8 = 0; // success
140            Ok((vec![status], ret, cost))
141        }
142        RequestType::Create1 | RequestType::Create2 => {
143            let gas = parser.take_u64().expect("expected gas");
144            let endowment = parser.take_u256().expect("expected endowment");
145            let salt = if req == RequestType::Create2 {
146                Some(parser.take_u256().expect("expected salt"))
147            } else {
148                None
149            };
150            let code = parser.take_rest();
151
152            let (addr, ret_val, cost) = host.create(code, endowment, salt, gas)?;
153            let mut res = vec![1u8];
154            res.extend_from_slice(addr.as_slice());
155            Ok((res, ret_val, cost))
156        }
157        RequestType::EmitLog => {
158            let topics = parser.take_u32().expect("expected topic count");
159            let mut hashes = Vec::with_capacity(topics as usize);
160            for _ in 0..topics {
161                hashes.push(parser.take_hash().expect("expected topic hash"));
162            }
163            host.emit_log(&hashes, parser.take_rest())?;
164            Ok((Vec::new(), Vec::new(), 0))
165        }
166        RequestType::AccountBalance => {
167            let address = parser.take_address().expect("expected address");
168            let (balance, cost) = host.account_balance(address)?;
169            Ok((balance.to_be_bytes::<32>().to_vec(), Vec::new(), cost))
170        }
171        RequestType::AccountCode => {
172            let address = parser.take_address().expect("expected address");
173            let gas = parser.take_u64().expect("expected gas");
174            let (code, cost) = host.account_code(address, gas)?;
175            Ok((Vec::new(), code, cost))
176        }
177        RequestType::AccountCodeHash => {
178            let address = parser.take_address().expect("expected address");
179            let (hash, cost) = host.account_code_hash(address)?;
180            Ok((hash.as_slice().to_vec(), Vec::new(), cost))
181        }
182        RequestType::AddPages => {
183            let pages = parser.take_u16().expect("expected pages");
184            let cost = host.add_pages(pages)?;
185            Ok((Vec::new(), Vec::new(), cost))
186        }
187        RequestType::CaptureHostIO => {
188            let start_ink = parser.take_u64().expect("expected start ink");
189            let end_ink = parser.take_u64().expect("expected end ink");
190            let name_len = parser.take_u32().expect("expected name len");
191            let args_len = parser.take_u32().expect("expected args len");
192            let outs_len = parser.take_u32().expect("expected outs len");
193            let name_bytes = parser.take_fixed(name_len as usize).expect("expected name");
194            let name = core::str::from_utf8(name_bytes).unwrap_or("");
195            let args = parser.take_fixed(args_len as usize).expect("expected args");
196            let outs = parser.take_fixed(outs_len as usize).expect("expected outs");
197            host.capture_hostio(name, args, outs, start_ink, end_ink);
198            Ok((Vec::new(), Vec::new(), 0))
199        }
200    }
201}
202
203/// Helper for parsing binary request payloads from the Stylus runtime.
204pub struct RequestParser<'a> {
205    data: &'a [u8],
206    offset: usize,
207}
208
209impl<'a> RequestParser<'a> {
210    pub fn new(data: &'a [u8]) -> Self {
211        Self { data, offset: 0 }
212    }
213
214    pub fn remaining(&self) -> &'a [u8] {
215        &self.data[self.offset..]
216    }
217
218    pub fn take_fixed(&mut self, n: usize) -> Option<&'a [u8]> {
219        if self.offset + n > self.data.len() {
220            return None;
221        }
222        let slice = &self.data[self.offset..self.offset + n];
223        self.offset += n;
224        Some(slice)
225    }
226
227    pub fn take_address(&mut self) -> Option<Address> {
228        let bytes = self.take_fixed(20)?;
229        Some(Address::from_slice(bytes))
230    }
231
232    pub fn take_hash(&mut self) -> Option<B256> {
233        let bytes = self.take_fixed(32)?;
234        Some(B256::from_slice(bytes))
235    }
236
237    pub fn take_u256(&mut self) -> Option<U256> {
238        let bytes = self.take_fixed(32)?;
239        Some(U256::from_be_slice(bytes))
240    }
241
242    pub fn take_u64(&mut self) -> Option<u64> {
243        let bytes = self.take_fixed(8)?;
244        Some(u64::from_be_bytes(bytes.try_into().ok()?))
245    }
246
247    pub fn take_u32(&mut self) -> Option<u32> {
248        let bytes = self.take_fixed(4)?;
249        Some(u32::from_be_bytes(bytes.try_into().ok()?))
250    }
251
252    pub fn take_u16(&mut self) -> Option<u16> {
253        let bytes = self.take_fixed(2)?;
254        Some(u16::from_be_bytes(bytes.try_into().ok()?))
255    }
256
257    pub fn take_rest(&mut self) -> &'a [u8] {
258        let rest = &self.data[self.offset..];
259        self.offset = self.data.len();
260        rest
261    }
262}