Skip to main content

neo_types/
stack_item.rs

1// Copyright (c) 2025-2026 R3E Network
2// Licensed under the MIT License
3
4//! NeoVM StackItem binary serialisation.
5//!
6//! The Neo N3 VM passes `StackItem` values across the host boundary as a
7//! binary form defined in C# `neo-project/neo/src/Neo/SmartContract/BinarySerializer.cs`.
8//! Contracts and devpack wrappers need to serialise args arrays (for
9//! `notify`, `Contract.Call`, etc.) into this form so the Neo VM host
10//! can read them.
11//!
12//! This module is the Rust devpack's re-implementation of that format.
13//! The tag bytes are the canonical `Neo.VM.Types.StackItemType` enum
14//! values; the integer payload is little-endian two's-complement to
15//! match .NET `BigInteger.ToByteArray()`:
16//!
17//! | Tag (1 byte) | Type |
18//! |--------------|------|
19//! | `0x00` | Any/Null (no payload) |
20//! | `0x20` | Boolean (followed by 0x00/0x01) |
21//! | `0x21` | Integer (varint length, then little-endian signed bytes) |
22//! | `0x28` | ByteString (varint length, then bytes) |
23//! | `0x30` | Buffer (same wire form as ByteString; writable buffer) |
24//! | `0x40` | Array (varint count, then nested items) |
25//! | `0x41` | Struct (varint count, then nested items) |
26//! | `0x48` | Map (varint count, then key/value item pairs) |
27//!
28//! Reference: C# `ApplicationEngine.Runtime.cs` `RuntimeNotify` calls
29//! `BinarySerializer.Serialize(writer, state, MaxNotificationSize, ...)`,
30//! and `Neo.VM.Types.StackItemType` for the tag values.
31
32use crate::{NeoArray, NeoString, NeoValue};
33
34/// Max serialised size for a notification (C#: `MaxNotificationSize = 1024`).
35pub const MAX_NOTIFICATION_SIZE: usize = 1024;
36
37/// Max items in a serialised Array/Struct (C#: `Limits.MaxStackSize`).
38pub const MAX_STACK_SIZE: usize = 1024;
39
40// Canonical `Neo.VM.Types.StackItemType` discriminants.
41const TAG_NULL: u8 = 0x00;
42const TAG_BOOLEAN: u8 = 0x20;
43const TAG_INTEGER: u8 = 0x21;
44const TAG_BYTESTRING: u8 = 0x28;
45const TAG_ARRAY: u8 = 0x40;
46const TAG_STRUCT: u8 = 0x41;
47
48/// Append a Neo `VarInt` length/count prefix.
49///
50/// Neo's `BinaryWriter.WriteVarInt` (used by `BinarySerializer` via
51/// `WriteVarBytes`) is **not** LEB128: values `< 0xFD` are a single
52/// byte, otherwise a `0xFD`/`0xFE`/`0xFF` marker is followed by a
53/// little-endian `u16`/`u32`/`u64`. Using LEB128 here would mis-encode
54/// any length `>= 0x80` and desynchronise the entire downstream stream.
55///
56/// This mirrors the same wire format the `wasm-neovm` compiler implements
57/// in its `core::encoding` module; the two are kept in lockstep by hand
58/// because the crates do not (yet) share a common low-level encoding crate.
59fn push_varint(out: &mut Vec<u8>, value: usize) {
60    let value = value as u64;
61    if value < 0xFD {
62        out.push(value as u8);
63    } else if value <= u16::MAX as u64 {
64        out.push(0xFD);
65        out.extend_from_slice(&(value as u16).to_le_bytes());
66    } else if value <= u32::MAX as u64 {
67        out.push(0xFE);
68        out.extend_from_slice(&(value as u32).to_le_bytes());
69    } else {
70        out.push(0xFF);
71        out.extend_from_slice(&value.to_le_bytes());
72    }
73}
74
75fn push_integer(out: &mut Vec<u8>, n: &crate::NeoInteger) {
76    let bigint = n.as_bigint();
77    out.push(TAG_INTEGER);
78    // Neo serialises integers as little-endian two's-complement, matching
79    // .NET `BigInteger.ToByteArray()`. Zero is the empty byte string (the
80    // VM's `Integer.GetSpan()` returns `Empty` for zero); `num-bigint`
81    // would otherwise emit a single `0x00`.
82    if bigint.sign() == num_bigint::Sign::NoSign {
83        push_varint(out, 0);
84        return;
85    }
86    let bytes = bigint.to_signed_bytes_le();
87    push_varint(out, bytes.len());
88    out.extend_from_slice(&bytes);
89}
90
91fn push_bytestring(out: &mut Vec<u8>, bytes: &[u8]) {
92    out.push(TAG_BYTESTRING);
93    push_varint(out, bytes.len());
94    out.extend_from_slice(bytes);
95}
96
97fn push_boolean(out: &mut Vec<u8>, b: bool) {
98    out.push(TAG_BOOLEAN);
99    out.push(if b { 0x01 } else { 0x00 });
100}
101
102fn push_stack_item(out: &mut Vec<u8>, value: &NeoValue) {
103    match value {
104        NeoValue::Null => out.push(TAG_NULL),
105        NeoValue::Boolean(b) => push_boolean(out, b.as_bool()),
106        NeoValue::Integer(i) => push_integer(out, i),
107        NeoValue::ByteString(bs) => push_bytestring(out, bs.as_slice()),
108        NeoValue::String(s) => push_bytestring(out, s.as_str().as_bytes()),
109        NeoValue::Array(arr) => {
110            out.push(TAG_ARRAY);
111            push_varint(out, arr.len());
112            for item in arr.iter() {
113                push_stack_item(out, item);
114            }
115        }
116        NeoValue::Struct(items) => {
117            // Structs serialise the same as Arrays but with a different
118            // outer tag (per C# `BinarySerializer.Serialize` for
119            // `StackItemType.Struct` = 0x41). The NeoVM distinguishes
120            // struct from array at the tag level; the contents are
121            // field values in declaration order.
122            out.push(TAG_STRUCT);
123            push_varint(out, items.len());
124            for (_name, value) in items.iter() {
125                push_stack_item(out, value);
126            }
127        }
128        NeoValue::Map(_) => {
129            // Maps cannot appear in a notification state (C# raises
130            // `InvalidOperationException`). Encode as Null so the VM
131            // receives a deterministic payload.
132            out.push(TAG_NULL);
133        }
134    }
135}
136
137/// Serialise a `NeoArray<NeoValue>` as a NeoVM `Array` StackItem.
138///
139/// The returned bytes match the binary form the C# Neo VM produces
140/// for an Array StackItem. Used by `System.Runtime.Notify` (B2 fix) and
141/// `System.Contract.Call` (B4 follow-up).
142pub fn serialise_array(items: &NeoArray<NeoValue>) -> Vec<u8> {
143    let mut out = Vec::with_capacity(items.len() * 4 + 2);
144    out.push(TAG_ARRAY);
145    push_varint(&mut out, items.len());
146    for item in items.iter() {
147        push_stack_item(&mut out, item);
148    }
149    out
150}
151
152/// Serialise a single StackItem (used for things like `Contract.Call`
153/// args that aren't wrapped in an outer array).
154pub fn serialise_value(value: &NeoValue) -> Vec<u8> {
155    let mut out = Vec::with_capacity(8);
156    push_stack_item(&mut out, value);
157    out
158}
159
160/// Serialise a UTF-8 event name + state array as a notification body.
161/// The body has the same shape as C# `RuntimeNotify` expects:
162/// `[event_name as NeoVM ByteString, state as Array StackItem]`.
163pub fn serialise_notification(event: &NeoString, state: &NeoArray<NeoValue>) -> Vec<u8> {
164    let mut out = Vec::with_capacity(event.as_str().len() + state.len() * 4 + 4);
165    // Outer container is an Array of 2 items.
166    out.push(TAG_ARRAY);
167    push_varint(&mut out, 2);
168    push_bytestring(&mut out, event.as_str().as_bytes());
169    push_stack_item(&mut out, &NeoValue::Array(state.clone()));
170    out
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::{NeoBoolean, NeoByteString, NeoInteger};
177
178    #[test]
179    fn varint_single_byte() {
180        // Neo VarInt encodes everything below 0xFD (253) as one byte,
181        // including 128..252 (where LEB128 would use two bytes).
182        let mut out = Vec::new();
183        push_varint(&mut out, 0);
184        push_varint(&mut out, 127);
185        push_varint(&mut out, 128);
186        push_varint(&mut out, 252);
187        assert_eq!(out, vec![0, 127, 128, 252]);
188    }
189
190    #[test]
191    fn varint_0xfd_marker() {
192        // 253 needs the 0xFD marker + little-endian u16.
193        let mut out = Vec::new();
194        push_varint(&mut out, 253);
195        assert_eq!(out, vec![0xFD, 0xFD, 0x00]);
196
197        let mut out = Vec::new();
198        push_varint(&mut out, 0x1234);
199        assert_eq!(out, vec![0xFD, 0x34, 0x12]);
200    }
201
202    #[test]
203    fn varint_0xfe_marker() {
204        // 0x10000 (just past u16) needs the 0xFE marker + little-endian u32.
205        let mut out = Vec::new();
206        push_varint(&mut out, 0x0001_0000);
207        assert_eq!(out, vec![0xFE, 0x00, 0x00, 0x01, 0x00]);
208    }
209
210    #[test]
211    fn bytestring_length_128_uses_single_byte_prefix() {
212        // Regression: a 128-byte payload must use a one-byte VarInt
213        // length (0x80), not the two-byte LEB128 form [0x80, 0x01].
214        let payload = vec![0xABu8; 128];
215        let bytes = serialise_value(&NeoValue::ByteString(crate::NeoByteString::from_slice(
216            &payload,
217        )));
218        assert_eq!(bytes[0], TAG_BYTESTRING);
219        assert_eq!(bytes[1], 0x80); // single-byte length prefix
220        assert_eq!(&bytes[2..], &payload[..]);
221        assert_eq!(bytes.len(), 130);
222    }
223
224    #[test]
225    fn tag_values_match_neo_stack_item_type() {
226        // Pin the canonical `Neo.VM.Types.StackItemType` discriminants so a
227        // future edit cannot silently reintroduce the 0x01/0x21 swap bug.
228        assert_eq!(TAG_NULL, 0x00);
229        assert_eq!(TAG_BOOLEAN, 0x20);
230        assert_eq!(TAG_INTEGER, 0x21);
231        assert_eq!(TAG_BYTESTRING, 0x28);
232        assert_eq!(TAG_ARRAY, 0x40);
233        assert_eq!(TAG_STRUCT, 0x41);
234    }
235
236    #[test]
237    fn integer_positive() {
238        let n = NeoInteger::new(42i32);
239        let mut out = Vec::new();
240        push_integer(&mut out, &n);
241        // tag + varint(len=1) + 0x2A
242        assert_eq!(out, vec![0x21, 0x01, 0x2A]);
243    }
244
245    #[test]
246    fn integer_zero_is_empty() {
247        // Neo's `Integer.GetSpan()` returns Empty for zero.
248        let mut out = Vec::new();
249        push_integer(&mut out, &NeoInteger::new(0i32));
250        assert_eq!(out, vec![0x21, 0x00]);
251    }
252
253    #[test]
254    fn integer_multibyte_is_little_endian() {
255        // 1000 = 0x03E8 → little-endian two's-complement minimal form
256        // is [0xE8, 0x03]. This is the NEP-17 `Transfer` amount shape;
257        // a big-endian bug would emit [0x03, 0xE8] and corrupt the value.
258        let mut out = Vec::new();
259        push_integer(&mut out, &NeoInteger::new(1000i32));
260        assert_eq!(out, vec![0x21, 0x02, 0xE8, 0x03]);
261
262        // 128 needs a zero sign byte so it stays positive: [0x80, 0x00].
263        let mut out = Vec::new();
264        push_integer(&mut out, &NeoInteger::new(128i32));
265        assert_eq!(out, vec![0x21, 0x02, 0x80, 0x00]);
266    }
267
268    #[test]
269    fn integer_negative_minimum_length() {
270        // -1 in two's complement is 0xFF (1 byte, endianness-agnostic).
271        let n = NeoInteger::new(-1i32);
272        let mut out = Vec::new();
273        push_integer(&mut out, &n);
274        assert_eq!(out, vec![0x21, 0x01, 0xFF]);
275
276        // -256 = 0xFF00 → little-endian two's-complement is [0x00, 0xFF].
277        let mut out = Vec::new();
278        push_integer(&mut out, &NeoInteger::new(-256i32));
279        assert_eq!(out, vec![0x21, 0x02, 0x00, 0xFF]);
280    }
281
282    #[test]
283    fn boolean() {
284        let mut out = Vec::new();
285        push_boolean(&mut out, true);
286        assert_eq!(out, vec![0x20, 0x01]);
287        push_boolean(&mut out, false);
288        assert_eq!(out, vec![0x20, 0x01, 0x20, 0x00]);
289    }
290
291    #[test]
292    fn empty_array() {
293        let arr = NeoArray::<NeoValue>::new();
294        let bytes = serialise_array(&arr);
295        assert_eq!(bytes, vec![TAG_ARRAY, 0x00]);
296    }
297
298    #[test]
299    fn array_with_int_and_bool() {
300        let mut arr = NeoArray::new();
301        arr.push(NeoValue::Integer(NeoInteger::new(7i32)));
302        arr.push(NeoValue::Boolean(NeoBoolean::new(true)));
303        let bytes = serialise_array(&arr);
304        // TAG_ARRAY, count=2, INT(7)={tag,1,7}, BOOL={tag,1}
305        assert_eq!(
306            bytes,
307            vec![TAG_ARRAY, 0x02, TAG_INTEGER, 0x01, 0x07, TAG_BOOLEAN, 0x01]
308        );
309    }
310
311    #[test]
312    fn notification_event_plus_state() {
313        let mut arr = NeoArray::new();
314        arr.push(NeoValue::Integer(NeoInteger::new(99i32)));
315        let event = NeoString::from_str("Transfer");
316        let bytes = serialise_notification(&event, &arr);
317        // outer array, count=2, "Transfer" as bytestring, state as nested array
318        let mut expected = vec![TAG_ARRAY, 0x02];
319        // "Transfer" bytestring
320        expected.push(TAG_BYTESTRING);
321        expected.push(8);
322        expected.extend_from_slice(b"Transfer");
323        // nested state array: [INT(99)]
324        expected.push(TAG_ARRAY);
325        expected.push(0x01);
326        expected.push(TAG_INTEGER);
327        expected.push(0x01);
328        expected.push(0x63);
329        assert_eq!(bytes, expected);
330    }
331
332    #[test]
333    fn bytestring_value() {
334        let v = NeoValue::ByteString(NeoByteString::from_slice(&[1, 2, 3]));
335        let bytes = serialise_value(&v);
336        assert_eq!(bytes, vec![TAG_BYTESTRING, 0x03, 0x01, 0x02, 0x03]);
337    }
338}