Skip to main content

neo_runtime/
storage.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! Storage convenience helpers built on top of the syscall layer.
5//!
6//! This module exposes two facades:
7//!
8//! - [`NeoStorage`] is the byte-string-typed API used by the host-side test
9//!   harness and by contracts that already manage `NeoByteString`/`Vec<u8>`
10//!   storage values themselves. It allocates through the standard Rust
11//!   allocator and is best suited to host (`cfg(not(target_arch = "wasm32"))`)
12//!   builds.
13//!
14//! - [`RawStorage`] is a heap-free facade that takes plain `&[u8]` slices and
15//!   writes results into caller-supplied buffers. On `wasm32` it lowers
16//!   directly to the translator-emitted Neo storage syscall helpers without
17//!   ever touching the wasm allocator. Production smart contracts that run on
18//!   Neo Express should prefer this path: it sidesteps the dlmalloc bookkeeping
19//!   that the wasm-to-NeoVM translator does not currently materialise on the
20//!   contract's NeoVM stack, so storage-heavy state transitions (multisig,
21//!   escrow, crowdfund, etc.) stay deploy-and-invoke-able rather than
22//!   "deploy-only".
23
24use neo_syscalls::NeoVMSyscall;
25use neo_types::*;
26
27#[cfg(target_arch = "wasm32")]
28#[link(wasm_import_module = "neo")]
29extern "C" {
30    #[link_name = "neo_storage_put_bytes"]
31    fn neo_storage_put_bytes(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32);
32
33    #[link_name = "neo_storage_delete_bytes"]
34    fn neo_storage_delete_bytes(key_ptr: i32, key_len: i32);
35
36    #[link_name = "neo_storage_get_into"]
37    fn neo_storage_get_into(key_ptr: i32, key_len: i32, out_ptr: i32, out_cap: i32) -> i32;
38
39    #[link_name = "raw_storage_put_i64"]
40    fn neo_raw_storage_put_i64(key: i64, value: i64);
41
42    #[link_name = "raw_storage_get_i64"]
43    fn neo_raw_storage_get_i64(key: i64) -> i64;
44
45    #[link_name = "raw_storage_has_i64"]
46    fn neo_raw_storage_has_i64(key: i64) -> i32;
47
48    #[link_name = "raw_storage_delete_i64"]
49    fn neo_raw_storage_delete_i64(key: i64);
50}
51
52/// Storage convenience helpers built on top of the syscall layer.
53pub struct NeoStorage;
54
55impl NeoStorage {
56    pub fn get_context() -> NeoResult<NeoStorageContext> {
57        NeoVMSyscall::storage_get_context()
58    }
59
60    pub fn get_read_only_context() -> NeoResult<NeoStorageContext> {
61        NeoVMSyscall::storage_get_read_only_context()
62    }
63
64    pub fn as_read_only(context: &NeoStorageContext) -> NeoResult<NeoStorageContext> {
65        NeoVMSyscall::storage_as_read_only(context)
66    }
67
68    pub fn get(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<NeoByteString> {
69        NeoVMSyscall::storage_get(context, key)
70    }
71
72    pub fn put(
73        context: &NeoStorageContext,
74        key: &NeoByteString,
75        value: &NeoByteString,
76    ) -> NeoResult<()> {
77        NeoVMSyscall::storage_put(context, key, value)
78    }
79
80    pub fn delete(context: &NeoStorageContext, key: &NeoByteString) -> NeoResult<()> {
81        NeoVMSyscall::storage_delete(context, key)
82    }
83
84    pub fn find(
85        context: &NeoStorageContext,
86        prefix: &NeoByteString,
87    ) -> NeoResult<NeoIterator<NeoValue>> {
88        NeoVMSyscall::storage_find(context, prefix)
89    }
90}
91
92/// Heap-free storage facade that operates on `&[u8]` slices.
93///
94/// `wasm32` lowers each call to the translator's `System.Storage.*` SYSCALL
95/// helpers directly, so contracts that use this path do not depend on the
96/// Rust allocator being functional inside NeoVM. Host (non-wasm32) builds
97/// route through the existing `NeoVMSyscall` simulation so unit tests behave
98/// the same as on wasm32.
99pub struct RawStorage;
100
101/// Outcome of [`RawStorage::get_into`].
102#[derive(Copy, Clone, PartialEq, Eq, Debug)]
103pub enum RawStorageGet {
104    /// Value was found and fully written into the caller buffer; the contained
105    /// `usize` is the number of bytes written.
106    Found(usize),
107    /// The runtime explicitly reported a null/missing value. Neo N3 storage
108    /// commonly surfaces absent keys as an empty byte string instead, so
109    /// callers must not rely on this variant for existence checks.
110    Missing,
111    /// Value exists but is larger than the caller buffer; the contained
112    /// `usize` is the byte length the caller must allocate before retrying.
113    BufferTooSmall(usize),
114}
115
116/// Fixed-capacity stack key builder for `RawStorage` keys.
117///
118/// This keeps contract samples on a heap-free path while centralizing the
119/// small `copy_nonoverlapping` block that fixed key construction needs.
120/// Push methods return `false` when the requested write would exceed capacity;
121/// existing bytes are left unchanged in that case.
122pub struct RawKeyBuilder<const N: usize> {
123    buf: core::mem::MaybeUninit<[u8; N]>,
124    len: usize,
125}
126
127impl<const N: usize> RawKeyBuilder<N> {
128    #[inline(always)]
129    pub const fn new() -> Self {
130        Self {
131            buf: core::mem::MaybeUninit::uninit(),
132            len: 0,
133        }
134    }
135
136    #[inline(always)]
137    pub fn push_bytes(&mut self, bytes: &[u8]) -> bool {
138        if bytes.len() > N - self.len {
139            return false;
140        }
141        unsafe {
142            core::ptr::copy_nonoverlapping(
143                bytes.as_ptr(),
144                self.buf.as_mut_ptr().cast::<u8>().add(self.len),
145                bytes.len(),
146            );
147        }
148        self.len += bytes.len();
149        true
150    }
151
152    #[inline(always)]
153    pub fn push_i64_le(&mut self, value: i64) -> bool {
154        self.push_bytes(&value.to_le_bytes())
155    }
156
157    #[inline(always)]
158    pub fn push_byte(&mut self, value: u8) -> bool {
159        if self.len == N {
160            return false;
161        }
162        unsafe {
163            *self.buf.as_mut_ptr().cast::<u8>().add(self.len) = value;
164        }
165        self.len += 1;
166        true
167    }
168
169    #[inline(always)]
170    pub fn as_slice(&self) -> &[u8] {
171        debug_assert!(self.len <= N);
172        unsafe { core::slice::from_raw_parts(self.buf.as_ptr().cast::<u8>(), self.len) }
173    }
174
175    #[inline(always)]
176    pub fn clear(&mut self) {
177        self.len = 0;
178    }
179
180    #[inline(always)]
181    pub fn len(&self) -> usize {
182        self.len
183    }
184
185    #[inline(always)]
186    pub fn is_empty(&self) -> bool {
187        self.len == 0
188    }
189}
190
191impl<const N: usize> Default for RawKeyBuilder<N> {
192    fn default() -> Self {
193        Self::new()
194    }
195}
196
197impl RawStorage {
198    /// Write `value` to `key` in the executing contract's persistent storage.
199    pub fn put(key: &[u8], value: &[u8]) {
200        #[cfg(target_arch = "wasm32")]
201        unsafe {
202            neo_storage_put_bytes(
203                key.as_ptr() as i32,
204                key.len() as i32,
205                value.as_ptr() as i32,
206                value.len() as i32,
207            );
208        }
209        #[cfg(not(target_arch = "wasm32"))]
210        {
211            let ctx = match NeoVMSyscall::storage_get_context() {
212                Ok(c) => c,
213                Err(_) => return,
214            };
215            let _ = NeoVMSyscall::storage_put(
216                &ctx,
217                &NeoByteString::from_slice(key),
218                &NeoByteString::from_slice(value),
219            );
220        }
221    }
222
223    /// Delete `key` from the executing contract's persistent storage.
224    pub fn delete(key: &[u8]) {
225        #[cfg(target_arch = "wasm32")]
226        unsafe {
227            neo_storage_delete_bytes(key.as_ptr() as i32, key.len() as i32);
228        }
229        #[cfg(not(target_arch = "wasm32"))]
230        {
231            let ctx = match NeoVMSyscall::storage_get_context() {
232                Ok(c) => c,
233                Err(_) => return,
234            };
235            let _ = NeoVMSyscall::storage_delete(&ctx, &NeoByteString::from_slice(key));
236        }
237    }
238
239    /// Read the value at `key` into `buf`.
240    ///
241    /// Returns one of:
242    /// - [`RawStorageGet::Found`] with the byte count actually written into
243    ///   `buf` when the key is present and the value fits.
244    /// - [`RawStorageGet::Missing`] only when the runtime explicitly reports
245    ///   null/missing; Neo N3 commonly returns zero bytes for absent keys.
246    /// - [`RawStorageGet::BufferTooSmall`] with the value's true length when
247    ///   `buf` cannot hold it; the value bytes are NOT copied in this case.
248    pub fn get_into(key: &[u8], buf: &mut [u8]) -> RawStorageGet {
249        #[cfg(target_arch = "wasm32")]
250        let actual = unsafe {
251            neo_storage_get_into(
252                key.as_ptr() as i32,
253                key.len() as i32,
254                buf.as_mut_ptr() as i32,
255                buf.len() as i32,
256            )
257        };
258        #[cfg(not(target_arch = "wasm32"))]
259        let actual = host_get_into(key, buf);
260
261        if actual == -1 {
262            RawStorageGet::Missing
263        } else if actual >= 0 {
264            RawStorageGet::Found(actual as usize)
265        } else {
266            RawStorageGet::BufferTooSmall((-actual) as usize)
267        }
268    }
269
270    /// Read an exact 8-byte little-endian `i64` at `key`. Returns `None` for
271    /// missing keys or for stored values whose length is not exactly 8.
272    pub fn get_i64(key: &[u8]) -> Option<i64> {
273        let mut buf = [0u8; 8];
274        match Self::get_into(key, &mut buf) {
275            RawStorageGet::Found(8) => Some(i64::from_le_bytes(buf)),
276            _ => None,
277        }
278    }
279
280    /// Read an exact 2-byte little-endian `u16` at `key`. Returns `None` for
281    /// missing keys or for stored values whose length is not exactly 2.
282    pub fn get_u16(key: &[u8]) -> Option<u16> {
283        let mut buf = [0u8; 2];
284        match Self::get_into(key, &mut buf) {
285            RawStorageGet::Found(2) => Some(u16::from_le_bytes(buf)),
286            _ => None,
287        }
288    }
289
290    /// Read an exact 1-byte boolean at `key`. Returns `None` for missing keys
291    /// or for stored values whose length is not exactly 1.
292    pub fn get_bool(key: &[u8]) -> Option<bool> {
293        let mut buf = [0u8; 1];
294        match Self::get_into(key, &mut buf) {
295            RawStorageGet::Found(1) => Some(buf[0] != 0),
296            _ => None,
297        }
298    }
299
300    /// Convenience: store an `i64` little-endian at `key`.
301    pub fn put_i64(key: &[u8], value: i64) {
302        Self::put(key, &value.to_le_bytes());
303    }
304
305    /// Convenience: store a `u16` little-endian at `key`.
306    pub fn put_u16(key: &[u8], value: u16) {
307        Self::put(key, &value.to_le_bytes());
308    }
309
310    /// Convenience: store a `bool` (encoded as a single 0/1 byte) at `key`.
311    pub fn put_bool(key: &[u8], value: bool) {
312        Self::put(key, &[value as u8]);
313    }
314
315    /// Store an `i64` value under an `i64` key without touching wasm linear
316    /// memory. On wasm32 this lowers directly to `System.Storage.Put`.
317    pub fn put_i64_key(key: i64, value: i64) {
318        #[cfg(target_arch = "wasm32")]
319        unsafe {
320            neo_raw_storage_put_i64(key, value);
321        }
322        #[cfg(not(target_arch = "wasm32"))]
323        host_put_i64_key(key, value);
324    }
325
326    /// Read an `i64` value from an `i64` key. Missing keys return `0`.
327    pub fn get_i64_key_or_zero(key: i64) -> i64 {
328        #[cfg(target_arch = "wasm32")]
329        unsafe {
330            neo_raw_storage_get_i64(key)
331        }
332        #[cfg(not(target_arch = "wasm32"))]
333        {
334            host_get_i64_key(key).unwrap_or(0)
335        }
336    }
337
338    /// Check whether an `i64` key has a stored integer value.
339    ///
340    /// Neo Express surfaces absent direct keys as empty bytes. The translator
341    /// therefore treats any non-empty `Storage.Get` result as present.
342    pub fn has_i64_key(key: i64) -> bool {
343        #[cfg(target_arch = "wasm32")]
344        unsafe {
345            neo_raw_storage_has_i64(key) != 0
346        }
347        #[cfg(not(target_arch = "wasm32"))]
348        {
349            host_has_i64_key(key)
350        }
351    }
352
353    /// Delete an `i64` key without touching wasm linear memory.
354    pub fn delete_i64_key(key: i64) {
355        #[cfg(target_arch = "wasm32")]
356        unsafe {
357            neo_raw_storage_delete_i64(key);
358        }
359        #[cfg(not(target_arch = "wasm32"))]
360        {
361            let ctx = match NeoVMSyscall::storage_get_context() {
362                Ok(c) => c,
363                Err(_) => return,
364            };
365            let key_bytes = neovm_i64_bytes(key);
366            let _ = NeoVMSyscall::storage_delete(&ctx, &NeoByteString::from_slice(&key_bytes));
367        }
368    }
369}
370
371#[cfg(not(target_arch = "wasm32"))]
372fn host_get_into(key: &[u8], buf: &mut [u8]) -> i32 {
373    let ctx = match NeoVMSyscall::storage_get_context() {
374        Ok(c) => c,
375        Err(_) => return -1,
376    };
377    let stored = match NeoVMSyscall::storage_try_get(&ctx, &NeoByteString::from_slice(key)) {
378        Ok(Some(b)) => b,
379        Ok(None) => return 0,
380        Err(_) => return -1,
381    };
382    let bytes = stored.as_slice();
383    if bytes.len() > buf.len() {
384        return -(bytes.len() as i32);
385    }
386    let len = bytes.len();
387    buf[..len].copy_from_slice(bytes);
388    len as i32
389}
390
391#[cfg(not(target_arch = "wasm32"))]
392fn host_put_i64_key(key: i64, value: i64) {
393    let ctx = match NeoVMSyscall::storage_get_context() {
394        Ok(c) => c,
395        Err(_) => return,
396    };
397    let key_bytes = neovm_i64_bytes(key);
398    let value_bytes = neovm_i64_bytes(value);
399    let _ = NeoVMSyscall::storage_put(
400        &ctx,
401        &NeoByteString::from_slice(&key_bytes),
402        &NeoByteString::from_slice(&value_bytes),
403    );
404}
405
406#[cfg(not(target_arch = "wasm32"))]
407fn host_get_i64_key(key: i64) -> Option<i64> {
408    let ctx = NeoVMSyscall::storage_get_context().ok()?;
409    let key_bytes = neovm_i64_bytes(key);
410    let stored = NeoVMSyscall::storage_try_get(&ctx, &NeoByteString::from_slice(&key_bytes))
411        .ok()
412        .flatten()?;
413    storage_bytes_to_i64(stored.as_slice())
414}
415
416#[cfg(not(target_arch = "wasm32"))]
417fn host_has_i64_key(key: i64) -> bool {
418    let ctx = match NeoVMSyscall::storage_get_context() {
419        Ok(c) => c,
420        Err(_) => return false,
421    };
422    let key_bytes = neovm_i64_bytes(key);
423    NeoVMSyscall::storage_try_get(&ctx, &NeoByteString::from_slice(&key_bytes))
424        .ok()
425        .flatten()
426        .map(|stored| !stored.as_slice().is_empty())
427        .unwrap_or(false)
428}
429
430#[cfg(not(target_arch = "wasm32"))]
431fn neovm_i64_bytes(value: i64) -> Vec<u8> {
432    if value == 0 {
433        return vec![0];
434    }
435
436    let mut bytes = value.to_le_bytes().to_vec();
437    while bytes.len() > 1 {
438        let last = *bytes.last().unwrap_or(&0);
439        let prev = bytes[bytes.len() - 2];
440        let redundant_positive = last == 0x00 && (prev & 0x80) == 0;
441        let redundant_negative = last == 0xff && (prev & 0x80) != 0;
442        if redundant_positive || redundant_negative {
443            bytes.pop();
444        } else {
445            break;
446        }
447    }
448    bytes
449}
450
451#[cfg(not(target_arch = "wasm32"))]
452fn storage_bytes_to_i64(bytes: &[u8]) -> Option<i64> {
453    match bytes.len() {
454        0 => None,
455        1..=8 => {
456            let sign_extend = bytes.last().copied().unwrap_or(0) & 0x80 != 0;
457            let mut buf = if sign_extend { [0xff; 8] } else { [0u8; 8] };
458            buf[..bytes.len()].copy_from_slice(bytes);
459            Some(i64::from_le_bytes(buf))
460        }
461        _ => None,
462    }
463}