Skip to main content

charms_app_runner/
lib.rs

1use anyhow::{Result, bail, ensure};
2use charms_data::{App, B32, Data, Transaction, is_simple_transfer, util};
3use rand::{RngExt, SeedableRng, rngs::StdRng};
4use sha2::{Digest, Sha256};
5use std::{
6    collections::BTreeMap,
7    io::Write,
8    sync::{Arc, Mutex},
9};
10use wasmi::{Caller, CompilationMode, Config, Engine, Extern, Linker, Memory, Module, Store};
11
12#[derive(Clone)]
13pub struct AppRunner {
14    pub count_cycles: bool,
15    pub engine: Engine,
16}
17
18#[derive(Clone)]
19struct HostState {
20    stdin: Arc<Mutex<Vec<u8>>>,    // Stdin buffer
21    stderr: Arc<Mutex<dyn Write>>, // Stderr buffer
22    prng: Arc<Mutex<StdRng>>,
23}
24
25// Helper functions for memory access
26fn read_i32(memory: &Memory, caller: &mut Caller<'_, HostState>, ptr: i32) -> Result<i32> {
27    let data = read_memory(memory, caller, ptr as usize, 4)?;
28    Ok(i32::from_le_bytes(data.try_into().unwrap()))
29}
30
31fn write_i32(
32    memory: &Memory,
33    caller: &mut Caller<'_, HostState>,
34    ptr: i32,
35    value: i32,
36) -> Result<()> {
37    let data = value.to_le_bytes();
38    write_memory(memory, caller, ptr as usize, &data)
39}
40
41fn read_memory(
42    memory: &Memory,
43    caller: &mut Caller<'_, HostState>,
44    ptr: usize,
45    len: usize,
46) -> Result<Vec<u8>> {
47    let mut buffer = vec![0; len];
48    memory.read(caller, ptr, &mut buffer)?;
49    Ok(buffer)
50}
51
52fn write_memory(
53    memory: &Memory,
54    caller: &mut Caller<'_, HostState>,
55    ptr: usize,
56    data: &[u8],
57) -> Result<()> {
58    memory.write(caller, ptr, data)?;
59    Ok(())
60}
61
62fn fd_read_impl(
63    mut caller: Caller<'_, HostState>,
64    fd: i32,
65    iovs: i32,
66    iovs_len: i32,
67    nread: i32,
68) -> Result<i32> {
69    if fd != 0 {
70        return Ok(-1); // Only handle stdin (fd=0)
71    }
72
73    let memory = caller
74        .get_export("memory")
75        .and_then(Extern::into_memory)
76        .ok_or_else(|| anyhow::anyhow!("No memory export"))?;
77
78    // First, read iovec addresses and lengths
79    let iov_size = 8;
80    let mut iov_info = Vec::new();
81    for i in 0..iovs_len {
82        let iov_addr = iovs + i * iov_size;
83        let buf_ptr = read_i32(&memory, &mut caller, iov_addr).unwrap() as usize;
84        let buf_len = read_i32(&memory, &mut caller, iov_addr + 4).unwrap() as usize;
85        iov_info.push((buf_ptr, buf_len));
86    }
87
88    // Then, read from stdin and prepare operations
89    let stdin_data = {
90        let state = caller.data();
91        let mut stdin = state.stdin.lock().unwrap();
92
93        let mut total_read = 0;
94        let mut operations = Vec::new();
95
96        for (buf_ptr, buf_len) in iov_info {
97            // Read from stdin buffer
98            let to_read = buf_len.min(stdin.len());
99            if to_read == 0 {
100                break; // No more input
101            }
102            let data = stdin.drain(..to_read).collect::<Vec<_>>();
103            operations.push((buf_ptr, data));
104            total_read += to_read;
105        }
106
107        (operations, total_read)
108    };
109
110    // Now perform memory writes without holding any borrows
111    for (buf_ptr, data) in stdin_data.0 {
112        write_memory(&memory, &mut caller, buf_ptr, &data).unwrap();
113    }
114
115    // Write number of bytes read to nread
116    write_i32(&memory, &mut caller, nread, stdin_data.1 as i32)?;
117
118    Ok(0) // Success
119}
120
121fn fd_write_impl(
122    mut caller: Caller<'_, HostState>,
123    fd: i32,
124    iovs: i32,
125    iovs_len: i32,
126    nwritten: i32,
127) -> Result<i32> {
128    if fd != 2 {
129        bail!("can only write to stderr"); // stderr fd=2
130    }
131
132    let memory = caller
133        .get_export("memory")
134        .and_then(Extern::into_memory)
135        .ok_or_else(|| anyhow::anyhow!("No memory export"))?;
136
137    // Read iovec array from WASM memory
138    let iov_size = 8; // sizeof(wasi_iovec_t) = ptr (i32) + len (i32)
139    let mut total_written = 0;
140    let mut all_data = Vec::new();
141
142    for i in 0..iovs_len {
143        let iov_addr = iovs + i * iov_size;
144        // Read iovec (buf: i32, buf_len: i32)
145        let buf_ptr = read_i32(&memory, &mut caller, iov_addr)? as usize;
146        let buf_len = read_i32(&memory, &mut caller, iov_addr + 4)? as usize;
147
148        // Read buffer from WASM memory
149        let data = read_memory(&memory, &mut caller, buf_ptr, buf_len)?;
150        all_data.extend_from_slice(&data);
151        total_written += buf_len;
152    }
153
154    // Now write to stderr without holding any borrows on caller
155    {
156        let state = caller.data_mut();
157        let mut stderr = state.stderr.lock().unwrap();
158        stderr.write_all(&all_data)?;
159    }
160
161    // Write number of bytes written to nwritten
162    write_i32(&memory, &mut caller, nwritten, total_written as i32)?;
163
164    Ok(0) // Success
165}
166
167fn fd_write(
168    caller: Caller<'_, HostState>,
169    fd: i32,
170    iovs: i32,
171    iovs_len: i32,
172    nwritten: i32,
173) -> i32 {
174    let result = fd_write_impl(caller, fd, iovs, iovs_len, nwritten);
175    result.unwrap_or_else(|e| {
176        eprintln!("error: {}", e);
177        -1
178    })
179}
180
181fn fd_read(caller: Caller<'_, HostState>, fd: i32, iovs: i32, iovs_len: i32, nread: i32) -> i32 {
182    fd_read_impl(caller, fd, iovs, iovs_len, nread).unwrap_or_else(|e| {
183        eprintln!("error: {}", e);
184        -1
185    })
186}
187
188fn environ_sizes_get_impl(
189    mut caller: Caller<'_, HostState>,
190    environc_ptr: i32,
191    environ_buf_size_ptr: i32,
192) -> Result<i32> {
193    let memory = caller
194        .get_export("memory")
195        .and_then(Extern::into_memory)
196        .ok_or_else(|| anyhow::anyhow!("No memory export"))?;
197
198    // Write 0 for number of environment variables
199    write_i32(&memory, &mut caller, environc_ptr, 0)?;
200    // Write 0 for total buffer size needed
201    write_i32(&memory, &mut caller, environ_buf_size_ptr, 0)?;
202
203    Ok(0) // Success
204}
205
206fn environ_get_impl(
207    _caller: Caller<'_, HostState>,
208    _environ_ptr: i32,
209    _environ_buf_ptr: i32,
210) -> Result<i32> {
211    // Nothing to write for empty environment
212    Ok(0) // Success
213}
214
215fn environ_sizes_get(
216    caller: Caller<'_, HostState>,
217    environc_ptr: i32,
218    environ_buf_size_ptr: i32,
219) -> i32 {
220    environ_sizes_get_impl(caller, environc_ptr, environ_buf_size_ptr).unwrap_or_else(|e| {
221        eprintln!("error: {}", e);
222        -1
223    })
224}
225
226fn environ_get(caller: Caller<'_, HostState>, environ_ptr: i32, environ_buf_ptr: i32) -> i32 {
227    environ_get_impl(caller, environ_ptr, environ_buf_ptr).unwrap_or_else(|e| {
228        eprintln!("error: {}", e);
229        -1
230    })
231}
232
233fn random_get(mut caller: Caller<'_, HostState>, buf: i32, buf_len: i32) -> i32 {
234    let memory = caller
235        .get_export("memory")
236        .and_then(Extern::into_memory)
237        .expect("No memory export");
238    let mut bytes = vec![0u8; buf_len as usize];
239    caller.data().prng.lock().unwrap().fill(&mut bytes);
240    memory
241        .write(&mut caller, buf as usize, &bytes)
242        .expect("failed to write random bytes");
243    0
244}
245
246const MAX_FUEL_PER_RUN: u64 = 1000000000;
247
248impl AppRunner {
249    pub fn new(count_cycles: bool) -> Self {
250        let mut config = Config::default();
251        if count_cycles {
252            config.consume_fuel(true);
253        }
254        config.compilation_mode(CompilationMode::Lazy);
255        Self {
256            count_cycles,
257            engine: Engine::new(&config),
258        }
259    }
260
261    pub fn vk(&self, binary: &[u8]) -> B32 {
262        let hash = Sha256::digest(binary);
263        B32(hash.into())
264    }
265
266    pub fn run(
267        &self,
268        app_binary: &[u8],
269        app: &App,
270        tx: &Transaction,
271        x: &Data,
272        w: &Data,
273    ) -> Result<u64> {
274        let vk = self.vk(app_binary);
275        ensure!(app.vk == vk, "app.vk mismatch");
276
277        let stdin_content = util::write(&(app, tx, x, w))?;
278
279        let prng_seed: [u8; 32] = Sha256::digest(&stdin_content).into();
280        let state = HostState {
281            stdin: Arc::new(Mutex::new(stdin_content)),
282            stderr: Arc::new(Mutex::new(std::io::stderr())),
283            prng: Arc::new(Mutex::new(StdRng::from_seed(prng_seed))),
284        };
285
286        let mut store = Store::new(&self.engine, state.clone());
287        if self.count_cycles {
288            store.set_fuel(MAX_FUEL_PER_RUN)?;
289        }
290        let mut linker = Linker::new(&self.engine);
291
292        linker.func_wrap("wasi_snapshot_preview1", "fd_write", fd_write)?;
293        linker.func_wrap("wasi_snapshot_preview1", "fd_read", fd_read)?;
294        linker.func_wrap("wasi_snapshot_preview1", "environ_get", environ_get)?;
295        linker.func_wrap(
296            "wasi_snapshot_preview1",
297            "environ_sizes_get",
298            environ_sizes_get,
299        )?;
300        linker.func_wrap(
301            "wasi_snapshot_preview1",
302            "proc_exit",
303            |_: Caller<'_, HostState>, _: i32| {},
304        )?;
305        linker.func_wrap("wasi_snapshot_preview1", "random_get", random_get)?;
306
307        let module = Module::new(&self.engine, app_binary)?;
308
309        let instance = linker.instantiate_and_start(&mut store, &module)?;
310
311        let Some(main_func) = instance.get_func(&store, "_start") else {
312            unreachable!("we should have a main function")
313        };
314        let result = main_func.typed::<(), ()>(&store)?.call(&mut store, ());
315
316        state.stderr.lock().unwrap().flush()?;
317
318        result.map_err(|e| anyhow::anyhow!("error running wasm: {:?}", e))?;
319
320        let cycles = match self.count_cycles {
321            true => MAX_FUEL_PER_RUN - store.get_fuel()?,
322            false => 0,
323        };
324        Ok(cycles)
325    }
326
327    pub fn run_all(
328        &self,
329        app_binaries: &BTreeMap<B32, Vec<u8>>,
330        tx: &Transaction,
331        app_public_inputs: &BTreeMap<App, Data>,
332        app_private_inputs: &BTreeMap<App, Data>,
333    ) -> Result<Vec<u64>> {
334        let empty = Data::empty();
335        let app_cycles = app_public_inputs
336            .iter()
337            .map(|(app, x)| {
338                let w = app_private_inputs.get(app).unwrap_or(&empty);
339                if x.is_empty() && w.is_empty() && is_simple_transfer(app, tx) {
340                    eprintln!("➡️  simple transfer w.r.t. app: {}", app);
341                    return Ok(0);
342                }
343                match app_binaries.get(&app.vk) {
344                    Some(app_binary) => {
345                        let cycles = self.run(app_binary, app, tx, x, w)?;
346                        eprintln!("✅  app contract satisfied: {}", app);
347                        Ok(cycles)
348                    }
349                    None => bail!("app binary not found: {}", app),
350                }
351            })
352            .collect::<Result<_>>()?;
353
354        Ok(app_cycles)
355    }
356}