oasis_runtime_sdk_contracts/abi/oasis/
storage.rs

1//! Storage imports.
2use std::convert::TryInto;
3
4use oasis_contract_sdk_types::storage::StoreKind;
5use oasis_runtime_sdk::{context::Context, storage::Store};
6
7use super::{memory::Region, OasisV1};
8use crate::{
9    abi::{gas, ExecutionContext},
10    store, Config, Error,
11};
12
13impl<Cfg: Config> OasisV1<Cfg> {
14    /// Link storage functions.
15    pub fn link_storage<C: Context>(
16        instance: &mut wasm3::Instance<'_, '_, ExecutionContext<'_, C>>,
17    ) -> Result<(), Error> {
18        // storage.get(store, key) -> value
19        let _ = instance.link_function(
20            "storage",
21            "get",
22            |ctx, (store, key): (u32, (u32, u32))| -> Result<u32, wasm3::Trap> {
23                // Make sure function was called in valid context.
24                let ec = ctx.context.ok_or(wasm3::Trap::Abort)?;
25                let store_kind: StoreKind = store.try_into().map_err(|_| wasm3::Trap::Abort)?;
26
27                ensure_key_size(ec, key.1)?;
28
29                // Charge base gas amount plus size-dependent gas.
30                let total_gas = (|| {
31                    let (base, key_base) = match store_kind {
32                        StoreKind::Public => (
33                            ec.params.gas_costs.wasm_public_storage_get_base,
34                            ec.params.gas_costs.wasm_public_storage_key_byte,
35                        ),
36                        StoreKind::Confidential => (
37                            ec.params.gas_costs.wasm_confidential_storage_get_base,
38                            ec.params.gas_costs.wasm_confidential_storage_key_byte,
39                        ),
40                    };
41                    let key = key_base.checked_mul(key.1.into())?;
42                    let total = base.checked_add(key)?;
43                    Some(total)
44                })()
45                .ok_or(wasm3::Trap::Abort)?;
46                gas::use_gas(ctx.instance, total_gas)?;
47
48                // Read from contract state.
49                let value = ctx.instance.runtime().try_with_memory(
50                    |memory| -> Result<_, wasm3::Trap> {
51                        let key = Region::from_arg(key).as_slice(&memory)?;
52                        with_instance_store(ec, store_kind, |store| store.get(key))
53                    },
54                )??;
55
56                let value = match value {
57                    Some(value) => value,
58                    None => return Ok(0),
59                };
60
61                // Charge gas for size of value.
62                let value_byte_cost = match store_kind {
63                    StoreKind::Public => ec.params.gas_costs.wasm_public_storage_value_byte,
64                    StoreKind::Confidential => {
65                        ec.params.gas_costs.wasm_confidential_storage_value_byte
66                    }
67                };
68                gas::use_gas(
69                    ctx.instance,
70                    value_byte_cost
71                        .checked_mul(value.len().try_into()?)
72                        .ok_or(wasm3::Trap::Abort)?,
73                )?;
74
75                // Create new region by calling `allocate`.
76                //
77                // This makes sure that the call context is unset to avoid any potential issues
78                // with reentrancy as attempting to re-enter one of the linked functions will fail.
79                let value_region = Self::allocate_and_copy(ctx.instance, &value)?;
80
81                // Return a pointer to the region.
82                Self::allocate_region(ctx.instance, value_region).map_err(|e| e.into())
83            },
84        );
85
86        // storage.insert(store, key, value)
87        let _ = instance.link_function(
88            "storage",
89            "insert",
90            |ctx, (store, key, value): (u32, (u32, u32), (u32, u32))| {
91                // Make sure function was called in valid context.
92                let ec = ctx.context.ok_or(wasm3::Trap::Abort)?;
93                let store_kind: StoreKind = store.try_into().map_err(|_| wasm3::Trap::Abort)?;
94
95                ensure_key_size(ec, key.1)?;
96                ensure_value_size(ec, value.1)?;
97
98                // Charge base gas amount plus size-dependent gas.
99                let total_gas = (|| {
100                    let (base, key_base, value_base) = match store_kind {
101                        StoreKind::Public => (
102                            ec.params.gas_costs.wasm_public_storage_insert_base,
103                            ec.params.gas_costs.wasm_public_storage_key_byte,
104                            ec.params.gas_costs.wasm_public_storage_value_byte,
105                        ),
106                        StoreKind::Confidential => (
107                            ec.params.gas_costs.wasm_confidential_storage_insert_base,
108                            ec.params.gas_costs.wasm_confidential_storage_key_byte,
109                            ec.params.gas_costs.wasm_confidential_storage_value_byte,
110                        ),
111                    };
112                    let key = key_base.checked_mul(key.1.into())?;
113                    let value = value_base.checked_mul(value.1.into())?;
114                    let total = base.checked_add(key)?.checked_add(value)?;
115                    Some(total)
116                })()
117                .ok_or(wasm3::Trap::Abort)?;
118                gas::use_gas(ctx.instance, total_gas)?;
119
120                // Insert into contract state.
121                ctx.instance
122                    .runtime()
123                    .try_with_memory(|memory| -> Result<(), wasm3::Trap> {
124                        let key = Region::from_arg(key).as_slice(&memory)?;
125                        let value = Region::from_arg(value).as_slice(&memory)?;
126                        with_instance_store(ec, store_kind, |store| store.insert(key, value))
127                    })??;
128
129                Ok(())
130            },
131        );
132
133        // storage.remove(store, key)
134        let _ = instance.link_function(
135            "storage",
136            "remove",
137            |ctx, (store, key): (u32, (u32, u32))| {
138                // Make sure function was called in valid context.
139                let ec = ctx.context.ok_or(wasm3::Trap::Abort)?;
140                let store_kind: StoreKind = store.try_into().map_err(|_| wasm3::Trap::Abort)?;
141
142                ensure_key_size(ec, key.1)?;
143
144                // Charge base gas amount plus size-dependent gas.
145                let total_gas = (|| {
146                    let (base, key_base) = match store_kind {
147                        StoreKind::Public => (
148                            ec.params.gas_costs.wasm_public_storage_remove_base,
149                            ec.params.gas_costs.wasm_public_storage_key_byte,
150                        ),
151                        StoreKind::Confidential => (
152                            ec.params.gas_costs.wasm_confidential_storage_remove_base,
153                            ec.params.gas_costs.wasm_confidential_storage_key_byte,
154                        ),
155                    };
156                    let key = key_base.checked_mul(key.1.into())?;
157                    let total = base.checked_add(key)?;
158                    Some(total)
159                })()
160                .ok_or(wasm3::Trap::Abort)?;
161                gas::use_gas(ctx.instance, total_gas)?;
162
163                // Remove from contract state.
164                ctx.instance
165                    .runtime()
166                    .try_with_memory(|memory| -> Result<(), wasm3::Trap> {
167                        let key = Region::from_arg(key).as_slice(&memory)?;
168                        with_instance_store(ec, store_kind, |store| store.remove(key))
169                    })??;
170
171                Ok(())
172            },
173        );
174
175        Ok(())
176    }
177}
178
179/// Run a closure with the contract instance store.
180fn with_instance_store<C, F, R>(
181    ec: &mut ExecutionContext<'_, C>,
182    store_kind: StoreKind,
183    f: F,
184) -> Result<R, wasm3::Trap>
185where
186    C: Context,
187    F: FnOnce(&mut dyn Store) -> R,
188{
189    store::with_instance_store(ec.tx_context, ec.instance_info, store_kind, |store| {
190        f(store)
191    })
192    .map_err(|err| {
193        // Propagate the underlying error.
194        ec.aborted = Some(err);
195        wasm3::Trap::Abort
196    })
197}
198
199/// Make sure that the key size is within the range specified in module parameters.
200fn ensure_key_size<C: Context>(
201    ec: &mut ExecutionContext<'_, C>,
202    size: u32,
203) -> Result<(), wasm3::Trap> {
204    if size > ec.params.max_storage_key_size_bytes {
205        ec.aborted = Some(Error::StorageKeyTooLarge(
206            size,
207            ec.params.max_storage_key_size_bytes,
208        ));
209        return Err(wasm3::Trap::Abort);
210    }
211    Ok(())
212}
213
214/// Make sure that the value size is within the range specified in module parameters.
215fn ensure_value_size<C: Context>(
216    ec: &mut ExecutionContext<'_, C>,
217    size: u32,
218) -> Result<(), wasm3::Trap> {
219    if size > ec.params.max_storage_value_size_bytes {
220        ec.aborted = Some(Error::StorageValueTooLarge(
221            size,
222            ec.params.max_storage_value_size_bytes,
223        ));
224        return Err(wasm3::Trap::Abort);
225    }
226    Ok(())
227}
228
229#[cfg(all(feature = "benchmarks", test))]
230mod bench {
231    extern crate test;
232    use super::*;
233    use std::{cell::RefCell, rc::Rc};
234    use test::Bencher;
235
236    use oasis_runtime_sdk::{context::Context, storage, testing::mock::Mock};
237
238    // cargo build --target wasm32-unknown-unknown --release
239    const BENCH_CODE: &[u8] = include_bytes!(
240        "../../../../../../tests/contracts/bench/target/wasm32-unknown-unknown/release/bench.wasm"
241    );
242
243    fn make_items(num: usize) -> Vec<(Vec<u8>, Vec<u8>)> {
244        let mut items = Vec::new();
245        for i in 0..num {
246            items.push((
247                format!("key{}", i).into_bytes(),
248                format!("value{}", i).into_bytes(),
249            ));
250        }
251        items
252    }
253
254    struct StoreContext<'a> {
255        store: Box<dyn storage::Store + 'a>,
256    }
257
258    #[bench]
259    fn bench_wasm_plain_get(b: &mut Bencher) {
260        // Set up storage stack and insert some items into it.
261        let mut mock = Mock::default();
262        let mut ctx = mock.create_ctx();
263        let inner = storage::PrefixStore::new(
264            storage::PrefixStore::new(
265                storage::PrefixStore::new(ctx.runtime_state(), "test module"),
266                "instance prefix",
267            ),
268            "type prefix",
269        );
270        let mut store_ctx = StoreContext {
271            store: Box::new(storage::HashedStore::<_, blake3::Hasher>::new(inner)),
272        };
273
274        let items = make_items(10_000);
275        for i in 0..10_000 {
276            let item = &items[i % items.len()];
277            store_ctx.store.insert(&item.0, &item.1);
278        }
279
280        // Set up wasm runtime.
281        let env =
282            wasm3::Environment::new().expect("creating a new wasm3 environment should succeed");
283        let module = env
284            .parse_module(BENCH_CODE)
285            .expect("parsing the code should succeed");
286        let rt: wasm3::Runtime<'_, StoreContext<'_>> = env
287            .new_runtime(1 * 1024 * 1024, None)
288            .expect("creating a new wasm3 runtime should succeed");
289        let mut instance = rt
290            .load_module(module)
291            .expect("instance creation should succeed");
292        let _ = instance.link_function(
293            "bench",
294            "plain_get",
295            |ctx, key: (u32, u32)| -> Result<u32, wasm3::Trap> {
296                let key = ctx.instance.runtime().try_with_memory(
297                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
298                        Ok(Region::from_arg(key)
299                            .as_slice(&memory)
300                            .map_err(|_| wasm3::Trap::Abort)?
301                            .into())
302                    },
303                )??;
304                let store_ctx = ctx.context.ok_or(wasm3::Trap::Abort)?;
305                match store_ctx.store.get(&key) {
306                    None => Ok(0),
307                    Some(value) => {
308                        let alloc = ctx
309                            .instance
310                            .find_function::<u32, u32>("alloc")
311                            .expect("finding alloc function should succeed");
312                        let val_len = value.len() as u32;
313                        let target_offset = alloc
314                            .call(val_len + std::mem::size_of::<u32>() as u32)
315                            .expect("alloc should succeed")
316                            as usize;
317
318                        ctx.instance.runtime().try_with_memory(
319                            |mut memory| -> Result<_, wasm3::Trap> {
320                                let len_bytes = &mut memory.as_slice_mut()
321                                    [target_offset..target_offset + std::mem::size_of::<u32>()];
322                                len_bytes.copy_from_slice(&val_len.to_le_bytes());
323
324                                let val_start = target_offset + std::mem::size_of::<u32>();
325                                let target =
326                                    &mut memory.as_slice_mut()[val_start..val_start + value.len()];
327                                target.copy_from_slice(&value);
328                                Ok(target_offset as u32)
329                            },
330                        )?
331                    }
332                }
333            },
334        );
335        let func = instance
336            .find_function::<(), ()>("bench_storage_get")
337            .expect("finding the entrypoint function should succeed");
338        b.iter(|| {
339            func.call_with_context(&mut store_ctx, ())
340                .expect("function call should succeed");
341        });
342    }
343
344    #[bench]
345    fn bench_wasm_plain_insert(b: &mut Bencher) {
346        // Set up storage stack and insert some items into it.
347        let mut mock = Mock::default();
348        let mut ctx = mock.create_ctx();
349        let inner = storage::PrefixStore::new(
350            storage::PrefixStore::new(
351                storage::PrefixStore::new(ctx.runtime_state(), "test module"),
352                "instance prefix",
353            ),
354            "type prefix",
355        );
356        let mut store_ctx = StoreContext {
357            store: Box::new(storage::HashedStore::<_, blake3::Hasher>::new(inner)),
358        };
359
360        // Set up wasm runtime.
361        let env =
362            wasm3::Environment::new().expect("creating a new wasm3 environment should succeed");
363        let module = env
364            .parse_module(BENCH_CODE)
365            .expect("parsing the code should succeed");
366        let rt: wasm3::Runtime<'_, StoreContext<'_>> = env
367            .new_runtime(1 * 1024 * 1024, None)
368            .expect("creating a new wasm3 runtime should succeed");
369        let mut instance = rt
370            .load_module(module)
371            .expect("instance creation should succeed");
372        let _ = instance.link_function(
373            "bench",
374            "plain_insert",
375            |ctx, (key, value): ((u32, u32), (u32, u32))| -> Result<u32, wasm3::Trap> {
376                let key = ctx.instance.runtime().try_with_memory(
377                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
378                        Ok(Region::from_arg(key)
379                            .as_slice(&memory)
380                            .map_err(|_| wasm3::Trap::Abort)?
381                            .into())
382                    },
383                )??;
384                let value = ctx.instance.runtime().try_with_memory(
385                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
386                        Ok(Region::from_arg(value)
387                            .as_slice(&memory)
388                            .map_err(|_| wasm3::Trap::Abort)?
389                            .into())
390                    },
391                )??;
392                let store_ctx = ctx.context.ok_or(wasm3::Trap::Abort)?;
393                store_ctx.store.insert(&key, &value);
394                Ok(0)
395            },
396        );
397        let func = instance
398            .find_function::<u32, u32>("bench_storage_insert")
399            .expect("finding the entrypoint function should succeed");
400        let mut counter_base: u32 = 0;
401        b.iter(|| {
402            counter_base += func
403                .call_with_context(&mut store_ctx, counter_base)
404                .expect("function call should succeed");
405        });
406    }
407
408    #[bench]
409    fn bench_wasm_plain_remove(b: &mut Bencher) {
410        // Set up storage stack and insert some items into it.
411        let mut mock = Mock::default();
412        let mut ctx = mock.create_ctx();
413        let inner = storage::PrefixStore::new(
414            storage::PrefixStore::new(
415                storage::PrefixStore::new(ctx.runtime_state(), "test module"),
416                "instance prefix",
417            ),
418            "type prefix",
419        );
420        let mut store_ctx = StoreContext {
421            store: Box::new(storage::HashedStore::<_, blake3::Hasher>::new(inner)),
422        };
423
424        for i in 0..2_000_000 {
425            store_ctx.store.insert(
426                format!("key{}", i).as_bytes(),
427                format!("value{}", i).as_bytes(),
428            );
429        }
430
431        // Set up wasm runtime.
432        let env =
433            wasm3::Environment::new().expect("creating a new wasm3 environment should succeed");
434        let module = env
435            .parse_module(BENCH_CODE)
436            .expect("parsing the code should succeed");
437        let rt: wasm3::Runtime<'_, StoreContext<'_>> = env
438            .new_runtime(1 * 1024 * 1024, None)
439            .expect("creating a new wasm3 runtime should succeed");
440        let mut instance = rt
441            .load_module(module)
442            .expect("instance creation should succeed");
443        let _ = instance.link_function(
444            "bench",
445            "plain_remove",
446            |ctx, key: (u32, u32)| -> Result<u32, wasm3::Trap> {
447                let key = ctx.instance.runtime().try_with_memory(
448                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
449                        Ok(Region::from_arg(key)
450                            .as_slice(&memory)
451                            .map_err(|_| wasm3::Trap::Abort)?
452                            .into())
453                    },
454                )??;
455                let store_ctx = ctx.context.ok_or(wasm3::Trap::Abort)?;
456                store_ctx.store.remove(&key);
457                Ok(0)
458            },
459        );
460        let func = instance
461            .find_function::<u32, u32>("bench_storage_remove")
462            .expect("finding the entrypoint function should succeed");
463        let mut counter_base: u32 = 0;
464        b.iter(|| {
465            counter_base += func
466                .call_with_context(&mut store_ctx, counter_base)
467                .expect("function call should succeed");
468        });
469    }
470
471    #[bench]
472    fn bench_nowasm_get(b: &mut Bencher) {
473        // Set up storage stack and insert some items into it.
474        let mut mock = Mock::default();
475        let mut ctx = mock.create_ctx();
476        let inner = storage::PrefixStore::new(
477            storage::PrefixStore::new(
478                storage::PrefixStore::new(ctx.runtime_state(), "test module"),
479                "instance prefix",
480            ),
481            "type prefix",
482        );
483        let mut store = Box::new(storage::HashedStore::<_, blake3::Hasher>::new(inner));
484
485        let items = make_items(10_000);
486        for i in 0..10_000 {
487            let item = &items[i % items.len()];
488            store.insert(&item.0, &item.1);
489        }
490        b.iter(move || {
491            for i in 0..5_000 {
492                let key = format!("key{}", i);
493                let exp_value = format!("value{}", i);
494                let value = store.get(key.as_bytes()).unwrap();
495                assert_eq!(exp_value.as_bytes(), value.as_slice());
496            }
497        });
498    }
499
500    #[bench]
501    fn bench_wasm_reach_gas_limit(_b: &mut Bencher) {
502        // Set up storage stack and insert some items into it.
503        let mut mock = Mock::default();
504        let mut ctx = mock.create_ctx();
505        let inner = storage::PrefixStore::new(
506            storage::PrefixStore::new(
507                storage::PrefixStore::new(ctx.runtime_state(), "test module"),
508                "instance prefix",
509            ),
510            "type prefix",
511        );
512        let mut store_ctx = StoreContext {
513            store: Box::new(storage::HashedStore::<_, blake3::Hasher>::new(inner)),
514        };
515
516        let params = crate::Parameters::default();
517        let params_cb = params.clone();
518
519        // Set up wasm runtime.
520        let mut module = walrus::ModuleConfig::new()
521            .generate_producers_section(false)
522            .parse(&BENCH_CODE)
523            .unwrap();
524        gas::transform(&mut module);
525        let instrumented_code = module.emit_wasm();
526        let env =
527            wasm3::Environment::new().expect("creating a new wasm3 environment should succeed");
528        let module = env
529            .parse_module(&instrumented_code)
530            .expect("parsing the code should succeed");
531        let rt: wasm3::Runtime<'_, StoreContext<'_>> = env
532            .new_runtime(1 * 1024 * 1024, None)
533            .expect("creating a new wasm3 runtime should succeed");
534        let mut instance = rt
535            .load_module(module)
536            .expect("instance creation should succeed");
537        let initial_gas: u64 = 1_000_000_000;
538        instance
539            .set_global(gas::EXPORT_GAS_LIMIT, initial_gas)
540            .expect("setting gas limit should succeed");
541
542        let bytes_written: Rc<RefCell<usize>> = Rc::new(RefCell::new(0));
543        let bytes_written_cb = bytes_written.clone();
544
545        let _ = instance.link_function(
546            "bench",
547            "plain_insert",
548            move |ctx, (key, value): ((u32, u32), (u32, u32))| -> Result<u32, wasm3::Trap> {
549                let key = ctx.instance.runtime().try_with_memory(
550                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
551                        Ok(Region::from_arg(key)
552                            .as_slice(&memory)
553                            .map_err(|_| wasm3::Trap::Abort)?
554                            .into())
555                    },
556                )??;
557                let value = ctx.instance.runtime().try_with_memory(
558                    |memory| -> Result<Vec<u8>, wasm3::Trap> {
559                        Ok(Region::from_arg(value)
560                            .as_slice(&memory)
561                            .map_err(|_| wasm3::Trap::Abort)?
562                            .into())
563                    },
564                )??;
565
566                let total_gas = (|| {
567                    let key_cost = params_cb
568                        .gas_costs
569                        .wasm_public_storage_key_byte
570                        .checked_mul(key.len() as u64)?;
571                    let value_cost = params_cb
572                        .gas_costs
573                        .wasm_public_storage_value_byte
574                        .checked_mul(value.len() as u64)?;
575                    let total = params_cb
576                        .gas_costs
577                        .wasm_public_storage_insert_base
578                        .checked_add(key_cost)?
579                        .checked_add(value_cost)?;
580                    Some(total)
581                })()
582                .ok_or(wasm3::Trap::Abort)?;
583                gas::use_gas(ctx.instance, total_gas)?;
584
585                *bytes_written_cb.borrow_mut() += value.len();
586                let store_ctx = ctx.context.ok_or(wasm3::Trap::Abort)?;
587                store_ctx.store.insert(&key, &value);
588                Ok(0)
589            },
590        );
591        let func = instance
592            .find_function::<u32, u32>("bench_storage_gas_consumer")
593            .expect("finding the entrypoint function should succeed");
594        let _ = func.call_with_context(&mut store_ctx, params.max_storage_value_size_bytes as u32);
595        let gas_limit: u64 = instance
596            .get_global(gas::EXPORT_GAS_LIMIT)
597            .expect("getting gas limit global should succeed");
598        let gas_limit_exhausted: u64 = instance
599            .get_global(gas::EXPORT_GAS_LIMIT_EXHAUSTED)
600            .expect("getting gas limit exhausted global should succeed");
601        println!(
602            "  storage waster: gas remaining {} [used: {}, exhausted flag: {}], value bytes written: {}",
603            gas_limit,
604            initial_gas - gas_limit,
605            gas_limit_exhausted,
606            *bytes_written.borrow()
607        );
608    }
609}