Skip to main content

ab_riscv_benchmarks/
host_utils.rs

1extern crate alloc;
2
3use ab_blake3::{CHUNK_LEN, OUT_LEN};
4use ab_contract_file::ContractInstruction;
5use ab_core_primitives::ed25519::{Ed25519PublicKey, Ed25519Signature};
6use ab_io_type::IoType;
7use ab_io_type::bool::Bool;
8use ab_riscv_interpreter::rv64::{Rv64InterpreterState, Rv64SystemInstructionHandler};
9use ab_riscv_interpreter::{
10    BasicInt, ExecutableInstruction, ExecutionError, FetchInstructionResult, InstructionFetcher,
11    ProgramCounter, ProgramCounterError, VirtualMemory, VirtualMemoryError,
12};
13use ab_riscv_primitives::instruction::Instruction;
14use ab_riscv_primitives::instruction::rv64::Rv64Instruction;
15use ab_riscv_primitives::registers::{Register, Registers};
16use alloc::vec::Vec;
17use core::marker::PhantomData;
18use core::mem::offset_of;
19use core::ops::ControlFlow;
20
21/// Contract file bytes
22pub const RISCV_CONTRACT_BYTES: &[u8] = {
23    #[cfg(target_env = "abundance")]
24    {
25        &[]
26    }
27    #[cfg(not(target_env = "abundance"))]
28    {
29        include_bytes!(env!("CONTRACT_PATH"))
30    }
31};
32
33// TODO: Generate similar helper data structures in the `#[contract]` macro itself, maybe introduce
34//  `SimpleInternalArgs` data trait for this or something
35/// Helper data structure for [`Benchmarks::blake3_hash_chunk()`] method
36///
37/// [`Benchmarks::blake3_hash_chunk()`]: crate::Benchmarks::blake3_hash_chunk
38#[derive(Debug, Copy, Clone)]
39#[repr(C)]
40pub struct Blake3HashChunkInternalArgs {
41    chunk_ptr: u64,
42    chunk_size: u32,
43    chunk_capacity: u32,
44    result_ptr: u64,
45    chunk: [u8; CHUNK_LEN],
46    result: [u8; OUT_LEN],
47}
48
49impl Blake3HashChunkInternalArgs {
50    /// Create a new instance
51    pub fn new(internal_args_addr: u64, chunk: [u8; CHUNK_LEN]) -> Self {
52        Self {
53            chunk_ptr: internal_args_addr + offset_of!(Self, chunk) as u64,
54            chunk_size: CHUNK_LEN as u32,
55            chunk_capacity: CHUNK_LEN as u32,
56            result_ptr: internal_args_addr + offset_of!(Self, result) as u64,
57            chunk,
58            result: [0; _],
59        }
60    }
61
62    /// Extract result
63    pub fn result(&self) -> [u8; OUT_LEN] {
64        self.result
65    }
66}
67
68// TODO: Generate similar helper data structures in the `#[contract]` macro itself, maybe introduce
69//  `SimpleInternalArgs` data trait for this or something
70/// Helper data structure for [`Benchmarks::ed25519_verify()`] method
71///
72/// [`Benchmarks::ed25519_verify()`]: crate::Benchmarks::ed25519_verify
73#[derive(Debug, Copy, Clone)]
74#[repr(C)]
75pub struct Ed25519VerifyInternalArgs {
76    pub public_key_ptr: u64,
77    pub public_key_size: u32,
78    pub public_key_capacity: u32,
79    pub signature_ptr: u64,
80    pub signature_size: u32,
81    pub signature_capacity: u32,
82    pub message_ptr: u64,
83    pub message_size: u32,
84    pub message_capacity: u32,
85    pub result_ptr: u64,
86    pub public_key: Ed25519PublicKey,
87    pub signature: Ed25519Signature,
88    pub message: [u8; OUT_LEN],
89    pub result: Bool,
90}
91
92impl Ed25519VerifyInternalArgs {
93    /// Create a new instance
94    pub fn new(
95        internal_args_addr: u64,
96        public_key: Ed25519PublicKey,
97        signature: Ed25519Signature,
98        message: [u8; OUT_LEN],
99    ) -> Self {
100        Self {
101            public_key_ptr: internal_args_addr + offset_of!(Self, public_key) as u64,
102            public_key_size: Ed25519PublicKey::SIZE as u32,
103            public_key_capacity: Ed25519PublicKey::SIZE as u32,
104            signature_ptr: internal_args_addr + offset_of!(Self, signature) as u64,
105            signature_size: Ed25519Signature::SIZE as u32,
106            signature_capacity: Ed25519Signature::SIZE as u32,
107            message_ptr: internal_args_addr + offset_of!(Self, message) as u64,
108            message_size: OUT_LEN as u32,
109            message_capacity: OUT_LEN as u32,
110            result_ptr: internal_args_addr + offset_of!(Self, result) as u64,
111            public_key,
112            signature,
113            message,
114            result: Bool::new(false),
115        }
116    }
117
118    /// Extract result
119    pub fn result(&self) -> Bool {
120        self.result
121    }
122}
123
124// Simple test memory implementation
125#[derive(Debug, Copy, Clone)]
126pub struct TestMemory<const MEMORY_SIZE: usize> {
127    data: [u8; MEMORY_SIZE],
128    base_addr: u64,
129}
130
131impl<const MEMORY_SIZE: usize> VirtualMemory for TestMemory<MEMORY_SIZE> {
132    fn read<T>(&self, address: u64) -> Result<T, VirtualMemoryError>
133    where
134        T: BasicInt,
135    {
136        let offset = address
137            .checked_sub(self.base_addr)
138            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })? as usize;
139
140        if offset + size_of::<T>() > self.data.len() {
141            return Err(VirtualMemoryError::OutOfBoundsRead { address });
142        }
143
144        // SAFETY: Only reading basic integers from initialized memory
145        unsafe {
146            Ok(self
147                .data
148                .as_ptr()
149                .cast::<T>()
150                .byte_add(offset)
151                .read_unaligned())
152        }
153    }
154
155    unsafe fn read_unchecked<T>(&self, address: u64) -> T
156    where
157        T: BasicInt,
158    {
159        // SAFETY: Guaranteed by function contract
160        unsafe {
161            let offset = address.unchecked_sub(self.base_addr) as usize;
162            self.data
163                .as_ptr()
164                .cast::<T>()
165                .byte_add(offset)
166                .read_unaligned()
167        }
168    }
169
170    fn write<T>(&mut self, address: u64, value: T) -> Result<(), VirtualMemoryError>
171    where
172        T: BasicInt,
173    {
174        let offset = address
175            .checked_sub(self.base_addr)
176            .ok_or(VirtualMemoryError::OutOfBoundsWrite { address })? as usize;
177
178        if offset + size_of::<T>() > self.data.len() {
179            return Err(VirtualMemoryError::OutOfBoundsWrite { address });
180        }
181
182        // SAFETY: Only writing basic integers to initialized memory
183        unsafe {
184            self.data
185                .as_mut_ptr()
186                .cast::<T>()
187                .byte_add(offset)
188                .write_unaligned(value);
189        }
190
191        Ok(())
192    }
193}
194
195impl<const MEMORY_SIZE: usize> TestMemory<MEMORY_SIZE> {
196    /// Create a new test memory instance with the specified base address
197    pub fn new(base_addr: u64) -> Self {
198        Self {
199            data: [0; _],
200            base_addr,
201        }
202    }
203
204    /// Get a slice of memory
205    pub fn get_bytes(&self, address: u64, size: usize) -> Result<&[u8], VirtualMemoryError> {
206        let offset = address
207            .checked_sub(self.base_addr)
208            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })? as usize;
209
210        if offset + size > self.data.len() {
211            return Err(VirtualMemoryError::OutOfBoundsRead { address });
212        }
213
214        Ok(&self.data[offset..][..size])
215    }
216
217    /// Get a mutable slice of memory
218    pub fn get_mut_bytes(
219        &mut self,
220        address: u64,
221        size: usize,
222    ) -> Result<&mut [u8], VirtualMemoryError> {
223        let offset = address
224            .checked_sub(self.base_addr)
225            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })? as usize;
226
227        if offset + size > self.data.len() {
228            return Err(VirtualMemoryError::OutOfBoundsRead { address });
229        }
230
231        Ok(&mut self.data[offset..][..size])
232    }
233}
234
235/// Eager instruction handler eagerly decodes all instructions upfront
236#[derive(Debug, Default, Clone)]
237pub struct EagerTestInstructionFetcher {
238    instructions: Vec<ContractInstruction>,
239    return_trap_address: u64,
240    base_addr: u64,
241    instruction_offset: usize,
242}
243
244impl<Memory> ProgramCounter<u64, Memory, &'static str> for EagerTestInstructionFetcher
245where
246    Memory: VirtualMemory,
247{
248    #[inline(always)]
249    fn get_pc(&self) -> u64 {
250        self.base_addr + self.instruction_offset as u64 * size_of::<u32>() as u64
251    }
252
253    #[inline]
254    fn set_pc(
255        &mut self,
256        _memory: &mut Memory,
257        pc: u64,
258    ) -> Result<ControlFlow<()>, ProgramCounterError<u64, &'static str>> {
259        let address = pc;
260
261        if address == self.return_trap_address {
262            return Ok(ControlFlow::Break(()));
263        }
264
265        if !address.is_multiple_of(size_of::<u32>() as u64) {
266            return Err(ProgramCounterError::UnalignedInstruction { address });
267        }
268
269        let offset = address
270            .checked_sub(self.base_addr)
271            .ok_or(VirtualMemoryError::OutOfBoundsRead { address })? as usize;
272        let instruction_offset = offset / size_of::<u32>();
273
274        if instruction_offset >= self.instructions.len() {
275            return Err(VirtualMemoryError::OutOfBoundsRead { address }.into());
276        }
277
278        self.instruction_offset = instruction_offset;
279
280        Ok(ControlFlow::Continue(()))
281    }
282}
283
284impl<Memory> InstructionFetcher<ContractInstruction, Memory, &'static str>
285    for EagerTestInstructionFetcher
286where
287    Memory: VirtualMemory,
288{
289    #[inline(always)]
290    fn fetch_instruction(
291        &mut self,
292        _memory: &mut Memory,
293    ) -> Result<
294        FetchInstructionResult<ContractInstruction>,
295        ExecutionError<u64, ContractInstruction, &'static str>,
296    > {
297        // SAFETY: Constructor guarantees that the last instruction is a jump, which means going
298        // through `Self::set_pc()` method that does bound check. Otherwise, advancing forward by
299        // one instruction can't result in out-of-bounds access.
300        let instruction = *unsafe { self.instructions.get_unchecked(self.instruction_offset) };
301        self.instruction_offset += 1;
302
303        Ok(FetchInstructionResult::Instruction(instruction))
304    }
305}
306
307impl EagerTestInstructionFetcher {
308    /// Create a new instance with the specified instructions and base address.
309    ///
310    /// Instructions are in the same order as they appear in the binary, and the base address
311    /// corresponds to the first instruction.
312    ///
313    /// `return_trap_address` is the address at which the interpreter will stop execution
314    /// (gracefully).
315    ///
316    /// # Safety
317    /// The program counter must be valid and aligned, the instructions processed must end with a
318    /// jump instruction.
319    #[inline(always)]
320    pub unsafe fn new(
321        instructions: Vec<ContractInstruction>,
322        return_trap_address: u64,
323        base_addr: u64,
324        pc: u64,
325    ) -> Self {
326        Self {
327            instructions,
328            return_trap_address,
329            base_addr,
330            instruction_offset: (pc - base_addr) as usize / size_of::<u32>(),
331        }
332    }
333}
334
335/// System instruction handler that does nothing
336#[derive(Debug, Clone, Copy)]
337pub struct NoopRv64SystemInstructionHandler<Instruction> {
338    _phantom: PhantomData<Instruction>,
339}
340
341impl<Reg> Default for NoopRv64SystemInstructionHandler<Reg> {
342    #[inline(always)]
343    fn default() -> Self {
344        Self {
345            _phantom: PhantomData,
346        }
347    }
348}
349
350impl<Reg, Memory, PC, CustomError> Rv64SystemInstructionHandler<Reg, Memory, PC, CustomError>
351    for NoopRv64SystemInstructionHandler<Rv64Instruction<Reg>>
352where
353    Reg: Register<Type = u64>,
354    [(); Reg::N]:,
355{
356    #[inline(always)]
357    fn handle_ecall(
358        &mut self,
359        _regs: &mut Registers<Reg>,
360        _memory: &mut Memory,
361        _program_counter: &mut PC,
362    ) -> Result<ControlFlow<()>, ExecutionError<u64, Rv64Instruction<Reg>, CustomError>> {
363        // SAFETY: Contracts are statically known to not contain `ecall` instructions
364        // unsafe { unreachable_unchecked() }
365        // For some known reason this is faster than `unreachable_unchecked()`
366        Ok(ControlFlow::Continue(()))
367    }
368}
369
370/// Execute [`ContractInstruction`]s
371#[expect(clippy::type_complexity)]
372pub fn execute<Memory, IF>(
373    state: &mut Rv64InterpreterState<
374        <ContractInstruction as Instruction>::Reg,
375        Memory,
376        IF,
377        NoopRv64SystemInstructionHandler<
378            Rv64Instruction<<ContractInstruction as Instruction>::Reg>,
379        >,
380        &'static str,
381    >,
382) -> Result<(), ExecutionError<u64, ContractInstruction, &'static str>>
383where
384    Memory: VirtualMemory,
385    IF: InstructionFetcher<ContractInstruction, Memory, &'static str>,
386{
387    loop {
388        let instruction = match state
389            .instruction_fetcher
390            .fetch_instruction(&mut state.memory)?
391        {
392            FetchInstructionResult::Instruction(instruction) => instruction,
393            FetchInstructionResult::ControlFlow(ControlFlow::Continue(())) => {
394                continue;
395            }
396            FetchInstructionResult::ControlFlow(ControlFlow::Break(())) => {
397                break;
398            }
399        };
400
401        match instruction.execute(state)? {
402            ControlFlow::Continue(()) => {
403                continue;
404            }
405            ControlFlow::Break(()) => {
406                break;
407            }
408        }
409    }
410
411    Ok(())
412}