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