diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index c27560b..0c6e69b 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -23,7 +23,10 @@ jobs: uses: actions/checkout@v4 - name: Install repository Rust toolchain - run: rustup toolchain install nightly-2025-02-14 --profile minimal --target thumbv7em-none-eabihf --target wasm32-unknown-unknown + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + target: thumbv7em-none-eabihf,wasm32-unknown-unknown + rustflags: "" - name: Compile the production no-std library run: cargo check -p tx-types --no-default-features --lib --target thumbv7em-none-eabihf diff --git a/BUILDING.md b/BUILDING.md index a6ca13c..cef4728 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,74 +1,57 @@ # Building tx-types -This document explains how to build the tx-types library with different dependency configurations. +`tx-types` can be built against two different Nockchain runtime sources: -## Default Build (Git Dependencies) +- `embedded` (default): the vendored `nockchain-minimal` crates in this + repository. Use this for WASM, embedded targets, and offline builds. +- `native`: upstream `SWPSCO/nockchain` at the pinned revision. Use this for + server-side native builds that already depend on the full runtime. -By default, the project uses nockchain dependencies from the git repository: +Both dependency sets are declared in `tx-types/Cargo.toml` and aliased onto the +canonical crate names in `tx-types/src/lib.rs`, so the same source compiles +against either one. No `[patch]` section is needed. -```bash -cargo build -cargo test -``` +## Feature matrix -This configuration matches the main branch and is suitable for most development work. +| Consumer | Features | +| --- | --- | +| ESP32 / no_std (crypto only) | `default-features = false` | +| WASM wallet (`wasm32-unknown-unknown`) | `default-features = false, features = ["std", "embedded"]` | +| Native server | `default-features = false, features = ["std", "native"]` | -## WASM Compilation (Local Dependencies) +`embedded` and `native` are mutually exclusive, and `std` requires exactly one +of them. Enabling both, or enabling `std` with neither, produces a +`compile_error!`. -For WASM compilation, you need to use local path dependencies from the `nockchain-minimal` submodule. This is required because WASM targets need access to the full source code. +`pokenoun` is re-exported in every configuration, including the crypto-only +no_std build. -### Switching to Local Dependencies +## Examples -Run the provided script: +Default (embedded + std): ```bash -./use-local-nockchain.sh +cargo build -p tx-types +cargo test -p tx-types --lib ``` -This will uncomment the `[patch]` section in `Cargo.toml` to override git dependencies with local paths. - -After switching, clean and rebuild: +WASM (embedded): ```bash -cargo clean -cargo build +cargo build -p tx-types \ + --no-default-features --features std,embedded \ + --target wasm32-unknown-unknown ``` -### Switching Back to Git Dependencies - -To switch back to git dependencies: +Native upstream runtime: ```bash -./use-git-nockchain.sh +cargo build -p tx-types --no-default-features --features std,native ``` -Then clean and rebuild: +Crypto-only no_std: ```bash -cargo clean -cargo build +cargo build -p tx-types --no-default-features +cargo build -p tx-types --no-default-features --target thumbv7em-none-eabihf ``` - -## How It Works - -The `Cargo.toml` file is configured with: - -1. **Default dependencies**: Git references to the nockchain repository - ```toml - noun-serde = { git = "https://github.com/SWPSCO/nockchain.git", rev = "..." } - ``` - -2. **Patch section (commented by default)**: Local path overrides - ```toml - # [patch."https://github.com/SWPSCO/nockchain.git"] - # noun-serde = { path = "./nockchain-minimal/crates/noun-serde" } - ``` - -When the patch section is uncommented, Cargo will use the local paths instead of fetching from git. This allows the same codebase to work in both configurations. - -## Why Two Configurations? - -- **Git dependencies**: Standard Rust practice, compatible with main branch, easier dependency management -- **Local dependencies**: Required for WASM compilation, allows local modifications to nockchain during development - -This approach avoids having separate branches for different build configurations. diff --git a/Cargo.toml b/Cargo.toml index f75aa03..4baa100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,17 +8,20 @@ edition = "2021" version = "0.1.0" [workspace.dependencies] -# Nockchain crates supplied by this repository -noun-serde = { path = "./nockchain-minimal/crates/noun-serde" } -noun-serde-derive = { path = "./nockchain-minimal/crates/noun-serde-derive" } -nockapp = { path = "./nockchain-minimal/crates/nockapp", default-features = false } -nockvm = { path = "./nockchain-minimal/crates/nockvm/rust/nockvm" } -nockchain-libp2p-io = { path = "./nockchain-minimal/crates/nockchain-libp2p-io", default-features = false } -nockchain-math = { path = "./nockchain-minimal/crates/nockchain-math" } -zkvm-jetpack = { path = "./nockchain-minimal/crates/zkvm-jetpack" } -ibig = { path = "./nockchain-minimal/crates/nockvm/rust/ibig", default-features = false } -murmur3 = { path = "./nockchain-minimal/crates/nockvm/rust/murmur3" } -nockvm_macros = { path = "./nockchain-minimal/crates/nockvm/rust/nockvm_macros" } +# Upstream Nockchain crates. tx-types itself declares both the upstream and +# vendored (nockchain-minimal) dependency sets and selects between them with +# the `native` / `embedded` features; these workspace entries remain for +# other workspace members and for reference. +noun-serde = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +noun-serde-derive = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +nockapp = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false } +nockvm = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +nockchain-libp2p-io = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false } +nockchain-math = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +zkvm-jetpack = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +ibig = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false } +murmur3 = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } +nockvm_macros = { git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382" } # Standard dependencies (matching nockchain-minimal workspace) aes = { version = "0.8.3", default-features = false } @@ -82,3 +85,4 @@ pokenoun = { path = "./pokenoun" } tracy-client-sys = { path = "./dummy-tracy/tracy-client-sys" } tracy-client = { path = "./dummy-tracy/tracy-client" } tracing-tracy = { path = "./dummy-tracy/tracing-tracy" } + diff --git a/nockchain-minimal/crates/nockapp/src/lib.rs b/nockchain-minimal/crates/nockapp/src/lib.rs index 5f50688..b183510 100644 --- a/nockchain-minimal/crates/nockapp/src/lib.rs +++ b/nockchain-minimal/crates/nockapp/src/lib.rs @@ -13,7 +13,7 @@ pub mod noun; pub mod utils; pub use bytes::*; -pub use nockvm::noun::Noun; +pub use nockvm::noun::{Noun, NounAllocator, NounSpace}; pub use noun::{AtomExt, JammedNoun, NounExt}; pub use utils::bytes::{ToBytes, ToBytesExt}; pub use utils::error::{CrownError, NockAppError, Result}; diff --git a/nockchain-minimal/crates/nockapp/src/noun/extensions.rs b/nockchain-minimal/crates/nockapp/src/noun/extensions.rs index 0dcf49f..dfe89ce 100644 --- a/nockchain-minimal/crates/nockapp/src/noun/extensions.rs +++ b/nockchain-minimal/crates/nockapp/src/noun/extensions.rs @@ -1,16 +1,86 @@ use core::str; use std::iter::Iterator; +use std::ptr::copy_nonoverlapping; use bincode::{Decode, Encode}; use bytes::Bytes; +use either::Either; +use intmap::IntMap; use nockvm::interpreter::Error; use nockvm::mem::NockStack; -use nockvm::noun::{Atom, IndirectAtom, NounAllocator, D}; +use nockvm::noun::{Atom, Cell, IndirectAtom, NounAllocator, NounSpace, D}; use nockvm::serialization::{cue, jam}; use crate::noun::slab::NounSlab; use crate::{Noun, Result, ToBytes, ToBytesExt}; +const CACHED_MUG_METADATA_MASK: u64 = 0x7fff_ffff; + +/// Copy a foreign noun into an allocator using the upstream space-aware +/// signature. The minimal runtime is single-arena, so `space` is ignored. +pub trait NounAllocatorExt { + fn copy_into(&mut self, noun: Noun, space: &NounSpace) -> Noun; +} + +impl NounAllocatorExt for A { + fn copy_into(&mut self, noun: Noun, _space: &NounSpace) -> Noun { + let mut copied: IntMap = IntMap::new(); + let mut stack = Vec::with_capacity(32); + let mut res = D(0); + stack.push((noun, &mut res as *mut Noun)); + while let Some((noun, dest)) = stack.pop() { + match noun.as_either_direct_allocated() { + Either::Left(direct) => unsafe { + *dest = direct.as_noun(); + }, + Either::Right(allocated) => match allocated.as_either() { + Either::Left(indirect) => { + let source = unsafe { indirect.to_raw_pointer() }; + let source_key = source as u64; + if let Some(copied_noun) = copied.get(source_key) { + unsafe { *dest = *copied_noun }; + continue; + } + let copied_mem = unsafe { self.alloc_indirect(indirect.size()) }; + unsafe { + copy_nonoverlapping(source, copied_mem, indirect.raw_size()); + *copied_mem &= CACHED_MUG_METADATA_MASK; + } + let copied_noun = unsafe { + IndirectAtom::from_raw_pointer(copied_mem) + .as_atom() + .as_noun() + }; + copied.insert(source_key, copied_noun); + unsafe { *dest = copied_noun }; + } + Either::Right(cell) => { + let source = unsafe { cell.to_raw_pointer() }; + let source_key = source as u64; + if let Some(copied_noun) = copied.get(source_key) { + unsafe { *dest = *copied_noun }; + continue; + } + let copied_mem = unsafe { self.alloc_cell() }; + unsafe { + copy_nonoverlapping(source, copied_mem, 1); + (*copied_mem).metadata &= CACHED_MUG_METADATA_MASK; + } + let copied_noun = unsafe { Cell::from_raw_pointer(copied_mem).as_noun() }; + copied.insert(source_key, copied_noun); + unsafe { *dest = copied_noun }; + unsafe { + stack.push((cell.tail(), &mut (*copied_mem).tail)); + stack.push((cell.head(), &mut (*copied_mem).head)); + } + } + }, + } + } + res + } +} + pub trait NounExt { fn cue_bytes(stack: &mut NockStack, bytes: &Bytes) -> std::result::Result; fn cue_bytes_slice(stack: &mut NockStack, bytes: &[u8]) -> std::result::Result; @@ -244,7 +314,7 @@ impl AsSlabVec for Noun { let mut slab_vec = Vec::new(); for noun in noun_list.list_iter() { let mut new_slab = NounSlab::new(); - new_slab.copy_into(noun); + new_slab.copy_into(noun, &NounSpace::empty()); slab_vec.push(new_slab); } slab_vec diff --git a/nockchain-minimal/crates/nockapp/src/noun/slab.rs b/nockchain-minimal/crates/nockapp/src/noun/slab.rs index 05210f9..954e0ff 100644 --- a/nockchain-minimal/crates/nockapp/src/noun/slab.rs +++ b/nockchain-minimal/crates/nockapp/src/noun/slab.rs @@ -11,7 +11,9 @@ use either::Either; use intmap::IntMap; use nockvm::mem::NockStack; use nockvm::mug::{calc_atom_mug_u32, calc_cell_mug_u32, get_mug, set_mug}; -use nockvm::noun::{Atom, Cell, CellMemory, DirectAtom, IndirectAtom, Noun, NounAllocator, D}; +use nockvm::noun::{ + Atom, Cell, CellMemory, DirectAtom, IndirectAtom, Noun, NounAllocator, NounSpace, D, +}; use nockvm::serialization::{met0_u64_to_usize, met0_usize}; use thiserror::Error; @@ -71,7 +73,7 @@ impl NounSlab { .list_iter() .map(|n| { let mut slab = Self::new(); - slab.copy_into(n); + slab.copy_into(n, &NounSpace::empty()); slab }) .collect() @@ -93,9 +95,10 @@ impl NounSlab { f: F, imports: (Noun, Noun, Noun), ) { - self.copy_into(imports.0); - self.copy_into(imports.1); - self.copy_into(imports.2); + let space = NounSpace::empty(); + self.copy_into(imports.0, &space); + self.copy_into(imports.1, &space); + self.copy_into(imports.2, &space); let new_root_base = f(imports, self.root); let new_root = nockvm::noun::T(self, &new_root_base); self.set_root(new_root); @@ -105,7 +108,8 @@ impl NounSlab { impl Clone for NounSlab { fn clone(&self) -> Self { let mut slab = Self::new(); - slab.copy_into(self.root); + let space = self.noun_space(); + slab.copy_into(self.root, &space); slab } } @@ -218,7 +222,7 @@ impl Default for NounSlab { impl From for NounSlab { fn from(noun: Noun) -> Self { let mut slab = Self::new(); - slab.copy_into(noun); + slab.copy_into(noun, &NounSpace::empty()); slab } } @@ -251,12 +255,16 @@ impl NounSlab { /// Copy the root from another slab into this slab, set this slab's root to the copied root pub fn copy_from_slab(&mut self, other: &NounSlab) { - self.copy_into(other.root); + let space = other.noun_space(); + self.copy_into(other.root, &space); } /// Copy a noun into this slab, only leaving references into the PMA. Set that noun as the root /// noun. - pub fn copy_into(&mut self, copy_root: Noun) -> Noun { + /// + /// The minimal runtime is single-arena, so `space` is accepted for API + /// compatibility with upstream and ignored. + pub fn copy_into(&mut self, copy_root: Noun, _space: &NounSpace) -> Noun { let mut copied: IntMap = IntMap::new(); // let mut copy_stack = vec![(copy_root, &mut self.root as *mut Noun)]; let mut copy_stack = vec![(copy_root, std::ptr::addr_of_mut!(self.root))]; @@ -1065,7 +1073,7 @@ mod tests { let test_noun = T(&mut slab, &[D(5), D(23)]); slab.set_root(test_noun); let mut copy_slab: NounSlab = NounSlab::new(); - copy_slab.copy_into(test_noun); + copy_slab.copy_into(test_noun, &NounSpace::empty()); } // Fails in Miri diff --git a/nockchain-minimal/crates/nockchain-libp2p-io/src/tip5_util.rs b/nockchain-minimal/crates/nockchain-libp2p-io/src/tip5_util.rs index 7a77d42..89e5441 100644 --- a/nockchain-minimal/crates/nockchain-libp2p-io/src/tip5_util.rs +++ b/nockchain-minimal/crates/nockchain-libp2p-io/src/tip5_util.rs @@ -1,7 +1,7 @@ use bs58; use ibig::{ubig, Stack, UBig}; use nockapp::NockAppError; -use nockvm::noun::Noun; +use nockvm::noun::{Noun, NounSpace}; use noun_serde::prelude::*; //TODO all this stuff would be useful as jets, which mostly just requires @@ -20,8 +20,8 @@ const P: u64 = 0xffffffff00000001; /// /// # Returns /// The Noun as a Base58 string -pub fn tip5_hash_to_base58(noun: Noun) -> Result { - let tuple: [u64; 5] = noun.decode()?; +pub fn tip5_hash_to_base58(noun: Noun, space: &NounSpace) -> Result { + let tuple: [u64; 5] = noun.decode(space)?; let decimal_value = base_p_to_decimal(tuple)?; let base58_string = ubig_to_base58(decimal_value); @@ -32,8 +32,9 @@ pub fn tip5_hash_to_base58(noun: Noun) -> Result { pub fn tip5_hash_to_base58_stack( stack: &mut S, noun: Noun, + space: &NounSpace, ) -> Result { - let tuple: [u64; 5] = noun.decode()?; + let tuple: [u64; 5] = noun.decode(space)?; let decimal_value = base_p_to_decimal_stack(stack, tuple)?; let base58_string = ubig_to_base58(decimal_value); diff --git a/nockchain-minimal/crates/nockchain-math/src/belt.rs b/nockchain-minimal/crates/nockchain-math/src/belt.rs index f4fed8b..fc642a0 100644 --- a/nockchain-minimal/crates/nockchain-math/src/belt.rs +++ b/nockchain-minimal/crates/nockchain-math/src/belt.rs @@ -4,7 +4,7 @@ use std::ops::{Add, Div, Mul, Neg, Sub}; use nockvm::jets::util::BAIL_EXIT; use nockvm::jets::JetErr; -use nockvm::noun::Noun; +use nockvm::noun::{Noun, NounSpace}; use noun_serde::{NounDecode, NounEncode}; use num_traits::Pow; use rkyv::{Archive, Deserialize, Serialize}; @@ -85,7 +85,7 @@ impl NounEncode for Belt { } impl NounDecode for Belt { - fn from_noun(noun: &nockvm::noun::Noun) -> Result { + fn from_noun(noun: &nockvm::noun::Noun, _space: &NounSpace) -> Result { let atom = noun .as_atom() .map_err(|_| noun_serde::NounDecodeError::ExpectedAtom)?; diff --git a/nockchain-minimal/crates/nockchain-math/src/convert.rs b/nockchain-minimal/crates/nockchain-math/src/convert.rs index 3adb920..c283879 100644 --- a/nockchain-minimal/crates/nockchain-math/src/convert.rs +++ b/nockchain-minimal/crates/nockchain-math/src/convert.rs @@ -1,7 +1,7 @@ use either::{Left, Right}; use nockvm::jets::util::BAIL_FAIL; use nockvm::jets::JetErr; -use nockvm::noun::{Atom, Cell, Error, IndirectAtom, Noun, Result, D}; +use nockvm::noun::{Atom, Cell, Error, IndirectAtom, Noun, NounSpace, Result, D}; use noun_serde::{NounDecode, NounEncode}; use crate::belt::*; @@ -331,7 +331,9 @@ impl TryFrom for BPolyVec { impl NounDecode for FPolyVec { fn from_noun( noun: &nockvm::noun::Noun, + space: &NounSpace, ) -> std::result::Result { + let _ = space; FPolyVec::try_from(noun.as_cell().expect("not a cell")) .map_err(|_| noun_serde::NounDecodeError::FPolyDecodeError) } @@ -349,7 +351,9 @@ impl NounEncode for FPolyVec { impl NounDecode for BPolyVec { fn from_noun( noun: &nockvm::noun::Noun, + space: &NounSpace, ) -> std::result::Result { + let _ = space; BPolyVec::try_from(noun.as_cell().expect("not a cell")) .map_err(|_| noun_serde::NounDecodeError::FPolyDecodeError) } diff --git a/nockchain-minimal/crates/nockchain-math/src/felt.rs b/nockchain-minimal/crates/nockchain-math/src/felt.rs index 65dc17f..c5b5bfc 100644 --- a/nockchain-minimal/crates/nockchain-math/src/felt.rs +++ b/nockchain-minimal/crates/nockchain-math/src/felt.rs @@ -1,6 +1,6 @@ use core::ops::{Add, Div, Mul, Neg, Sub}; -use nockvm::noun::IndirectAtom; +use nockvm::noun::{IndirectAtom, NounSpace}; use noun_serde::{NounDecode, NounEncode}; use num_traits::{MulAdd, Pow}; @@ -25,7 +25,7 @@ impl NounEncode for Felt { // Custom NounDecode implementation for Felt impl NounDecode for Felt { - fn from_noun(noun: &nockvm::noun::Noun) -> Result { + fn from_noun(noun: &nockvm::noun::Noun, _space: &NounSpace) -> Result { let felt_slice = noun.as_felt()?; let res: [Belt; 3] = [felt_slice[0], felt_slice[1], felt_slice[2]]; Ok(Felt(res)) diff --git a/nockchain-minimal/crates/nockchain-math/src/fpoly.rs b/nockchain-minimal/crates/nockchain-math/src/fpoly.rs index 0c55028..16e8cfa 100644 --- a/nockchain-minimal/crates/nockchain-math/src/fpoly.rs +++ b/nockchain-minimal/crates/nockchain-math/src/fpoly.rs @@ -302,9 +302,9 @@ pub fn fpeval(a: &[Felt], x: Felt) -> Felt { } #[inline(always)] -pub fn lift_to_fpoly(belts: HoonList, res: &mut [Felt]) { +pub fn lift_to_fpoly(belts: HoonList, res: &mut [Felt], space: &nockvm::noun::NounSpace) { for (i, b) in belts.into_iter().enumerate() { - let belt = Belt::from_noun(&b).unwrap_or_else(|err| { + let belt = Belt::from_noun(&b, space).unwrap_or_else(|err| { panic!( "Panicked with {err:?} at {}:{} (git sha: {:?})", file!(), diff --git a/nockchain-minimal/crates/nockchain-math/src/mary.rs b/nockchain-minimal/crates/nockchain-math/src/mary.rs index 69553f2..20c705e 100644 --- a/nockchain-minimal/crates/nockchain-math/src/mary.rs +++ b/nockchain-minimal/crates/nockchain-math/src/mary.rs @@ -1,4 +1,4 @@ -use nockvm::noun::{IndirectAtom, Noun, NounAllocator}; +use nockvm::noun::{IndirectAtom, Noun, NounAllocator, NounSpace}; use noun_serde::{NounDecode, NounDecodeError, NounEncode}; use crate::belt::Belt; @@ -92,7 +92,7 @@ impl TryFrom> for &mut [Felt] { } impl NounDecode for Mary { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { Mary::try_from(*noun).map_err(|_| NounDecodeError::MaryDecodeError) } } diff --git a/nockchain-minimal/crates/nockchain-math/src/shape.rs b/nockchain-minimal/crates/nockchain-math/src/shape.rs index 96513a0..a41f568 100644 --- a/nockchain-minimal/crates/nockchain-math/src/shape.rs +++ b/nockchain-minimal/crates/nockchain-math/src/shape.rs @@ -1,7 +1,7 @@ use nockapp::Noun; use nockvm::jets::list::util::flop; use nockvm::jets::JetErr; -use nockvm::noun::{NounAllocator, D, T}; +use nockvm::noun::{NounAllocator, NounSpace, D, T}; use noun_serde::NounEncode; pub fn dyck(stack: &mut A, t: Noun) -> Result { @@ -21,21 +21,25 @@ fn dyck_recursive(stack: &mut A, t: Noun, vec: Noun) -> Result } } -pub fn leaf_sequence(stack: &mut A, t: Noun) -> Result { +pub fn leaf_sequence( + stack: &mut A, + t: Noun, + space: &NounSpace, +) -> Result { let mut leaf: Vec = Vec::::new(); - do_leaf_sequence(t, &mut leaf)?; + do_leaf_sequence(t, &mut leaf, space)?; let res = leaf.to_noun(stack); Ok(res) } -pub fn do_leaf_sequence(noun: Noun, vec: &mut Vec) -> Result<(), JetErr> { +pub fn do_leaf_sequence(noun: Noun, vec: &mut Vec, space: &NounSpace) -> Result<(), JetErr> { if noun.is_atom() { - vec.push(noun.as_atom()?.as_u64()?); + vec.push(noun.in_space(space).as_atom()?.as_u64()?); Ok(()) } else { - let cell = noun.as_cell()?; - do_leaf_sequence(cell.head(), vec)?; - do_leaf_sequence(cell.tail(), vec)?; + let cell = noun.in_space(space).as_cell()?; + do_leaf_sequence(cell.head().noun(), vec, space)?; + do_leaf_sequence(cell.tail().noun(), vec, space)?; Ok(()) } } diff --git a/nockchain-minimal/crates/nockchain-math/src/tip5/hash.rs b/nockchain-minimal/crates/nockchain-math/src/tip5/hash.rs index 1d053df..7afbc1d 100644 --- a/nockchain-minimal/crates/nockchain-math/src/tip5/hash.rs +++ b/nockchain-minimal/crates/nockchain-math/src/tip5/hash.rs @@ -1,7 +1,7 @@ use nockapp::Noun; use nockvm::jets::list::util::{lent, weld}; use nockvm::jets::JetErr; -use nockvm::noun::{NounAllocator, D, T}; +use nockvm::noun::{NounAllocator, NounSpace, D, T}; use noun_serde::{NounDecode, NounEncode}; use super::*; @@ -125,8 +125,12 @@ pub fn hash_10(input_vec: &mut Vec) -> [u64; 5] { tip5_calc_digest(&sponge) } -pub fn hash_noun_varlen(stack: &mut A, n: Noun) -> Result { - let leaf = leaf_sequence(stack, n)?; +pub fn hash_noun_varlen( + stack: &mut A, + n: Noun, + space: &NounSpace, +) -> Result { + let leaf = leaf_sequence(stack, n, space)?; let dyck = dyck(stack, n)?; let size = lent(leaf).map(|x| D(x as u64))?; @@ -134,21 +138,38 @@ pub fn hash_noun_varlen(stack: &mut A, n: Noun) -> Result( stack: &mut A, n: Noun, + space: &NounSpace, ) -> Result<[u64; 5], JetErr> { - let noun_res = hash_noun_varlen(stack, n)?; - let digest = <[u64; 5]>::from_noun(&noun_res)?; + let noun_res = hash_noun_varlen(stack, n, space)?; + let digest = <[u64; 5]>::from_noun(&noun_res, space)?; Ok(digest) } -pub fn hash_belts_list(alloc: &mut A, input: Noun) -> Result { - let mut input_vec = >::from_noun(&input)?; +pub fn hash_belts_list( + alloc: &mut A, + input: Noun, + space: &NounSpace, +) -> Result { + let mut input_vec = >::from_noun(&input, space)?; let digest = hash_varlen(&mut input_vec); let res = digest.to_noun(alloc); Ok(res) } + +/// Hash exactly [`RATE`] limbs with the TIP5 `hash-10` algorithm. +pub fn hash_ten_cell(input: [u64; RATE]) -> [u64; DIGEST_LENGTH] { + let mut input_vec: Vec = input.iter().map(|value| Belt(*value)).collect(); + hash_10(&mut input_vec) +} + +/// Hash a flat slice of limbs with the TIP5 variable-length algorithm. +pub fn hash_belts_slice(input: &[u64]) -> [u64; DIGEST_LENGTH] { + let mut input_vec: Vec = input.iter().map(|value| Belt(*value)).collect(); + hash_varlen(&mut input_vec) +} diff --git a/nockchain-minimal/crates/nockvm/rust/nockvm/src/noun.rs b/nockchain-minimal/crates/nockvm/rust/nockvm/src/noun.rs index c057f75..b3eb7af 100644 --- a/nockchain-minimal/crates/nockvm/rust/nockvm/src/noun.rs +++ b/nockchain-minimal/crates/nockvm/rust/nockvm/src/noun.rs @@ -1540,6 +1540,15 @@ pub trait NounAllocator: Sized { /** Allocate space for a struct in a stack frame */ unsafe fn alloc_struct(&mut self, count: usize) -> *mut T; + + /** The space nouns allocated by this allocator belong to. + * + * The minimal runtime resolves pointers directly, so the default empty + * space is sufficient; allocators with several arenas can override it. + */ + fn noun_space(&self) -> NounSpace { + NounSpace::empty() + } } /** @@ -1700,3 +1709,263 @@ mod test { ); } } + +// --------------------------------------------------------------------------- +// Space-scoped handle API (additive compatibility shim) +// +// The upstream nockchain API threads a `NounSpace` through noun traversal so +// that indirect atoms can be resolved without relying on ambient state. The +// vendored minimal runtime is single-arena and resolves pointers directly, so +// the space is carried by the handles but ignored when reading. +// --------------------------------------------------------------------------- + +/// Allocation context for a noun. +/// +/// The minimal runtime is single-arena; the recorded ranges are retained for +/// API compatibility but are not required to resolve pointers. +#[derive(Clone, Default, Debug)] +pub struct NounSpace { + extra_ptr_ranges: Vec<(usize, usize)>, +} + +impl NounSpace { + /// An empty space. Valid for direct atoms and for the minimal runtime's + /// single-arena nouns. + pub fn empty() -> Self { + Self { + extra_ptr_ranges: Vec::new(), + } + } + + /// Record additional pointer ranges belonging to this space. + pub fn with_extra_ptr_ranges(mut self, ranges: Vec<(usize, usize)>) -> Self { + self.extra_ptr_ranges = ranges; + self + } + + /// Pointer ranges recorded for this space. + pub fn extra_ptr_ranges(&self) -> &[(usize, usize)] { + &self.extra_ptr_ranges + } +} + +/// A noun paired with the space it belongs to. +#[derive(Copy, Clone)] +pub struct NounHandle<'a> { + noun: Noun, + space: &'a NounSpace, +} + +impl<'a> NounHandle<'a> { + pub fn new(noun: Noun, space: &'a NounSpace) -> Self { + Self { noun, space } + } + + pub fn noun(self) -> Noun { + self.noun + } + + pub fn space(self) -> &'a NounSpace { + self.space + } + + pub fn is_direct(self) -> bool { + self.noun.is_direct() + } + + pub fn is_atom(self) -> bool { + self.noun.is_atom() + } + + pub fn is_cell(self) -> bool { + self.noun.is_cell() + } + + pub fn is_allocated(self) -> bool { + self.noun.is_allocated() + } + + pub fn as_atom(self) -> Result> { + self.noun + .as_atom() + .map(|atom| AtomHandle::new(atom, self.space)) + } + + pub fn as_cell(self) -> Result> { + self.noun + .as_cell() + .map(|cell| CellHandle::new(cell, self.space)) + } + + pub fn atom(self) -> Option> { + self.noun.atom().map(|atom| AtomHandle::new(atom, self.space)) + } + + pub fn cell(self) -> Option> { + self.noun.cell().map(|cell| CellHandle::new(cell, self.space)) + } + + pub fn as_noun(self) -> Noun { + self.noun + } + + pub fn eq_bytes(self, bytes: impl AsRef<[u8]>) -> bool { + self.as_atom().map(|atom| atom.eq_bytes(bytes)).unwrap_or(false) + } +} + +/// A cell paired with the space it belongs to. +#[derive(Copy, Clone)] +pub struct CellHandle<'a> { + cell: Cell, + space: &'a NounSpace, +} + +impl<'a> CellHandle<'a> { + pub fn new(cell: Cell, space: &'a NounSpace) -> Self { + Self { cell, space } + } + + pub fn cell(self) -> Cell { + self.cell + } + + pub fn space(self) -> &'a NounSpace { + self.space + } + + pub fn as_noun(self) -> NounHandle<'a> { + NounHandle::new(self.cell.as_noun(), self.space) + } + + pub fn head(self) -> NounHandle<'a> { + NounHandle::new(self.cell.head(), self.space) + } + + pub fn tail(self) -> NounHandle<'a> { + NounHandle::new(self.cell.tail(), self.space) + } + + pub fn head_tail(self) -> (NounHandle<'a>, NounHandle<'a>) { + (self.head(), self.tail()) + } +} + +/// An atom paired with the space it belongs to. +#[derive(Copy, Clone)] +pub struct AtomHandle<'a> { + atom: Atom, + space: &'a NounSpace, +} + +impl<'a> AtomHandle<'a> { + pub fn new(atom: Atom, space: &'a NounSpace) -> Self { + Self { atom, space } + } + + pub fn atom(self) -> Atom { + self.atom + } + + pub fn space(self) -> &'a NounSpace { + self.space + } + + pub fn as_noun(self) -> NounHandle<'a> { + NounHandle::new(self.atom.as_noun(), self.space) + } + + pub fn is_direct(self) -> bool { + self.atom.is_direct() + } + + pub fn is_indirect(self) -> bool { + self.atom.is_indirect() + } + + pub fn is_normalized(self) -> bool { + self.atom.is_normalized() + } + + pub fn as_ne_bytes(&self) -> &[u8] { + self.atom.as_ne_bytes() + } + + pub fn to_ne_bytes(self) -> Vec { + self.atom.to_ne_bytes() + } + + pub fn to_be_bytes(self) -> Vec { + self.atom.to_be_bytes() + } + + pub fn to_le_bytes(self) -> Vec { + self.atom.to_le_bytes() + } + + pub fn as_u64(self) -> Result { + self.atom.as_u64() + } + + pub fn size(self) -> usize { + self.atom.size() + } + + pub fn bit_size(self) -> usize { + self.atom.bit_size() + } + + pub fn data_pointer(self) -> *const u64 { + self.atom.data_pointer() + } + + pub fn as_bitslice(&self) -> &BitSlice { + self.atom.as_bitslice() + } + + pub fn eq_bytes>(&self, bytes: B) -> bool { + let bytes_ref = bytes.as_ref(); + let atom_bytes = self.atom.as_ne_bytes(); + if bytes_ref.len() > atom_bytes.len() { + return false; + } + if bytes_ref.len() == atom_bytes.len() { + return atom_bytes == bytes_ref; + } + if atom_bytes[bytes_ref.len()..].iter().any(|b| *b != 0) { + return false; + } + &atom_bytes[..bytes_ref.len()] == bytes_ref + } + + pub fn into_string(self) -> std::result::Result { + std::str::from_utf8(self.atom.as_ne_bytes()) + .map(|string| string.trim_end_matches('\0').to_string()) + } + + pub fn to_bytes_until_nul(self) -> std::result::Result, std::str::Utf8Error> { + std::str::from_utf8(self.atom.as_ne_bytes()) + .map(|bytes| bytes.trim_end_matches('\0').as_bytes().to_vec()) + } +} + +impl Noun { + /// Pair this noun with the space it was allocated in. + pub fn in_space<'a>(&self, space: &'a NounSpace) -> NounHandle<'a> { + NounHandle::new(*self, space) + } +} + +impl Cell { + /// Pair this cell with the space it was allocated in. + pub fn in_space<'a>(&self, space: &'a NounSpace) -> CellHandle<'a> { + CellHandle::new(*self, space) + } +} + +impl Atom { + /// Pair this atom with the space it was allocated in. + pub fn in_space<'a>(self, space: &'a NounSpace) -> AtomHandle<'a> { + AtomHandle::new(self, space) + } +} diff --git a/nockchain-minimal/crates/noun-serde-derive/src/lib.rs b/nockchain-minimal/crates/noun-serde-derive/src/lib.rs index 11a135e..671d1b6 100644 --- a/nockchain-minimal/crates/noun-serde-derive/src/lib.rs +++ b/nockchain-minimal/crates/noun-serde-derive/src/lib.rs @@ -442,7 +442,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { let field_name = &field_names[0]; let field_type = &field_types[0]; quote! { - let #field_name = <#field_type as ::noun_serde::NounDecode>::from_noun(noun)?; + let #field_name = <#field_type as ::noun_serde::NounDecode>::from_noun(noun, space)?; Ok(Self { #field_name }) } } else { @@ -492,7 +492,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { let axis = custom_axis.unwrap_or(default_axis); quote! { - let #name = <#ty as ::noun_serde::NounDecode>::from_noun(&::nockvm::noun::Slots::slot(&cell, #axis)?)?; + let #name = <#ty as ::noun_serde::NounDecode>::from_noun(&::nockvm::noun::Slots::slot(&cell, #axis)?, space)?; } }); @@ -510,7 +510,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { if field_count == 1 { let field_type = &fields.unnamed[0].ty; quote! { - let field_0 = <#field_type as ::noun_serde::NounDecode>::from_noun(noun)?; + let field_0 = <#field_type as ::noun_serde::NounDecode>::from_noun(noun, space)?; Ok(Self(field_0)) } } else { @@ -570,7 +570,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { quote! { let field_noun = ::nockvm::noun::Slots::slot(&cell, #axis) .map_err(|_| ::noun_serde::NounDecodeError::FieldError(stringify!(#field_ident).to_string(), "Missing field".into()))?; - let #field_ident = <#field_type as ::noun_serde::NounDecode>::from_noun(&field_noun) + let #field_ident = <#field_type as ::noun_serde::NounDecode>::from_noun(&field_noun, space) .map_err(|e| ::noun_serde::NounDecodeError::FieldError(stringify!(#field_ident).to_string(), e.to_string()))?; } }); @@ -654,7 +654,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { let axis = custom_axis.unwrap_or(default_axis); quote! { let field_cell = ::nockvm::noun::Slots::slot(&data, #axis)?.as_cell()?; - let #name = <#ty as ::noun_serde::NounDecode>::from_noun(&field_cell.tail())?; + let #name = <#ty as ::noun_serde::NounDecode>::from_noun(&field_cell.tail(), space)?; } }); @@ -682,7 +682,8 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { tail_cell.head() } else { tail_cell.tail() - } + }, + space, )?; )* Ok(Self::#variant_name { #(#field_names),* }) @@ -708,7 +709,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { quote! { tag if tag == #tag => { if let Ok(cell) = noun.as_cell() { - let value = <#ty as ::noun_serde::NounDecode>::from_noun(&cell.tail())?; + let value = <#ty as ::noun_serde::NounDecode>::from_noun(&cell.tail(), space)?; Ok(Self::#variant_name(value)) } else { Err(::noun_serde::NounDecodeError::ExpectedCell) @@ -774,7 +775,7 @@ pub fn derive_noun_decode(input: TokenStream) -> TokenStream { // Generate the impl block let expanded = quote! { impl ::noun_serde::NounDecode for #name { - fn from_noun(noun: &::nockvm::noun::Noun) -> Result { + fn from_noun(noun: &::nockvm::noun::Noun, space: &::nockvm::noun::NounSpace) -> Result { #decode_impl } } diff --git a/nockchain-minimal/crates/noun-serde/src/lib.rs b/nockchain-minimal/crates/noun-serde/src/lib.rs index de498e5..e61a874 100644 --- a/nockchain-minimal/crates/noun-serde/src/lib.rs +++ b/nockchain-minimal/crates/noun-serde/src/lib.rs @@ -6,7 +6,7 @@ pub mod wallet; use nockapp::utils::make_tas; use nockapp::{AtomExt, NockAppError}; #[allow(unused_imports)] -use nockvm::noun::{Atom, FullDebugCell, Noun, NounAllocator, Slots, D, T}; +use nockvm::noun::{Atom, FullDebugCell, Noun, NounAllocator, NounSpace, Slots, D, T}; use nockvm::noun::{NO, YES}; use nockvm::jets::util::BAIL_FAIL; use nockvm::jets::JetErr; @@ -19,12 +19,12 @@ pub mod prelude { // Trait extensions for Noun pub trait NounSerdeDecodeExt { - fn decode(&self) -> Result; + fn decode(&self, space: &NounSpace) -> Result; } impl NounSerdeDecodeExt for nockvm::noun::Noun { - fn decode(&self) -> Result { - T::from_noun(self) + fn decode(&self, space: &NounSpace) -> Result { + T::from_noun(self, space) } } @@ -45,7 +45,7 @@ pub trait NounEncode { /// Trait for types that can be decoded from a Noun pub trait NounDecode: Sized { /// Try to decode this value from a Noun - fn from_noun(noun: &Noun) -> Result; + fn from_noun(noun: &Noun, space: &NounSpace) -> Result; } /// Error that can occur during Noun decoding @@ -94,6 +94,12 @@ impl From for NockAppError { } } +impl From for NounDecodeError { + fn from(err: std::str::Utf8Error) -> Self { + NounDecodeError::Custom(err.to_string()) + } +} + impl From for NounDecodeError { fn from(err: nockvm::noun::Error) -> Self { NounDecodeError::Custom(err.to_string()) @@ -108,7 +114,7 @@ impl NounEncode for Noun { } impl NounDecode for Noun { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { Ok(*noun) } } @@ -122,7 +128,7 @@ impl NounEncode for u64 { } impl NounDecode for u64 { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { match noun.as_atom() { Ok(atom) => atom .as_u64() @@ -140,7 +146,7 @@ impl NounEncode for u32 { } impl NounDecode for u32 { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { match noun.as_atom() { Ok(atom) => atom .as_u64() @@ -159,7 +165,7 @@ impl NounEncode for String { } impl NounDecode for String { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { match noun.as_atom() { Ok(atom) => atom .into_string() @@ -170,10 +176,10 @@ impl NounDecode for String { } impl NounDecode for (X, Y) { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let a = X::from_noun(&cell.slot(2)?)?; - let b = Y::from_noun(&cell.slot(3)?)?; + let a = X::from_noun(&cell.slot(2)?, space)?; + let b = Y::from_noun(&cell.slot(3)?, space)?; Ok((a, b)) } } @@ -188,11 +194,11 @@ impl NounEncode for (X, Y) { } impl NounDecode for (X, Y, Z) { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let a = X::from_noun(&cell.slot(2)?)?; - let b = Y::from_noun(&cell.slot(6)?)?; - let c = Z::from_noun(&cell.slot(7)?)?; + let a = X::from_noun(&cell.slot(2)?, space)?; + let b = Y::from_noun(&cell.slot(6)?, space)?; + let c = Z::from_noun(&cell.slot(7)?, space)?; Ok((a, b, c)) } } @@ -209,12 +215,12 @@ impl NounEncode for (X, Y, Z) { } impl NounDecode for (W, X, Y, Z) { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let a = W::from_noun(&cell.slot(2)?)?; - let b = X::from_noun(&cell.slot(6)?)?; - let c = Y::from_noun(&cell.slot(14)?)?; - let d = Z::from_noun(&cell.slot(15)?)?; + let a = W::from_noun(&cell.slot(2)?, space)?; + let b = X::from_noun(&cell.slot(6)?, space)?; + let c = Y::from_noun(&cell.slot(14)?, space)?; + let d = Z::from_noun(&cell.slot(15)?, space)?; Ok((a, b, c, d)) } } @@ -235,13 +241,13 @@ impl NounEncode for impl NounDecode for (V, W, X, Y, Z) { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let a = V::from_noun(&cell.slot(2)?)?; - let b = W::from_noun(&cell.slot(6)?)?; - let c = X::from_noun(&cell.slot(14)?)?; - let d = Y::from_noun(&cell.slot(30)?)?; - let e = Z::from_noun(&cell.slot(31)?)?; + let a = V::from_noun(&cell.slot(2)?, space)?; + let b = W::from_noun(&cell.slot(6)?, space)?; + let c = X::from_noun(&cell.slot(14)?, space)?; + let d = Y::from_noun(&cell.slot(30)?, space)?; + let e = Z::from_noun(&cell.slot(31)?, space)?; Ok((a, b, c, d, e)) } } @@ -266,14 +272,14 @@ impl impl NounDecode for (U, V, W, X, Y, Z) { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let a = U::from_noun(&cell.slot(2)?)?; - let b = V::from_noun(&cell.slot(6)?)?; - let c = W::from_noun(&cell.slot(14)?)?; - let d = X::from_noun(&cell.slot(30)?)?; - let e = Y::from_noun(&cell.slot(62)?)?; - let f = Z::from_noun(&cell.slot(63)?)?; + let a = U::from_noun(&cell.slot(2)?, space)?; + let b = V::from_noun(&cell.slot(6)?, space)?; + let c = W::from_noun(&cell.slot(14)?, space)?; + let d = X::from_noun(&cell.slot(30)?, space)?; + let e = Y::from_noun(&cell.slot(62)?, space)?; + let f = Z::from_noun(&cell.slot(63)?, space)?; Ok((a, b, c, d, e, f)) } } @@ -308,7 +314,7 @@ impl NounEncode for bool { } impl NounDecode for bool { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { trace!("Decoding bool from noun: {:?}", noun); match noun.as_atom() { Ok(atom) => { @@ -348,7 +354,7 @@ impl NounEncode for Option { } impl NounDecode for Option { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // First check if it's an atom 0 (None) if let Ok(atom) = noun.as_atom() { match atom.as_u64() { @@ -373,7 +379,7 @@ impl NounDecode for Option { )); } - let value = T::from_noun(&cell.tail())?; + let value = T::from_noun(&cell.tail(), space)?; Ok(Some(value)) } } @@ -388,14 +394,14 @@ impl NounEncode for Vec { } impl NounDecode for Vec { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let mut result = Vec::new(); let mut current = noun; #[allow(unused_assignments)] let mut current_tail = None; while let Ok(cell) = current.as_cell() { - let item = T::from_noun(&cell.head())?; + let item = T::from_noun(&cell.head(), space)?; result.push(item); current_tail = Some(cell.tail()); current = current_tail.as_ref().unwrap(); @@ -425,8 +431,8 @@ impl NounEncode for Vec { } impl NounDecode for Vec { - fn from_noun(noun: &Noun) -> Result { - let nums = Vec::::from_noun(noun)?; + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + let nums = Vec::::from_noun(noun, space)?; Ok(nums.into_iter().map(|x| x as u8).collect()) } } @@ -465,7 +471,7 @@ impl NounDecode for Vec { /// /// let mut stack = NockStack::new(8 << 10 << 10, 0); /// let encoded = map.to_noun(&mut stack); -/// let decoded = HashMap::::from_noun(&encoded).unwrap(); +/// let decoded = HashMap::::from_noun(&encoded, space).unwrap(); /// assert_eq!(map, decoded); /// ``` impl NounEncode for HashMap @@ -548,7 +554,7 @@ where K: std::hash::Hash + Eq + std::fmt::Debug, V: std::fmt::Debug, { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { trace!("\nDecoding HashMap from noun: {:?}", noun); // Handle empty tree case if let Ok(atom) = noun.as_atom() { @@ -568,6 +574,7 @@ where >( node: &Noun, map: &mut HashMap, + space: &NounSpace, ) -> Result<(), NounDecodeError> { // Base case: empty branch if let Ok(atom) = node.as_atom() { @@ -593,8 +600,8 @@ where trace!("Key type: {:?}", std::any::type_name::()); trace!("Value type: {:?}", std::any::type_name::()); - let key = K::from_noun(&pair.head())?; - let value = V::from_noun(&pair.tail())?; + let key = K::from_noun(&pair.head(), space)?; + let value = V::from_noun(&pair.tail(), space)?; trace!("Key: {:?}, Value: {:?}", key, value); map.insert(key, value); @@ -604,13 +611,13 @@ where let right = &rest.tail(); // Recursively process left and right branches - traverse_tree(left, map)?; - traverse_tree(right, map)?; + traverse_tree(left, map, space)?; + traverse_tree(right, map, space)?; Ok(()) } - traverse_tree(noun, &mut map)?; + traverse_tree(noun, &mut map, space)?; Ok(map) } } @@ -640,7 +647,7 @@ where /// /// let mut stack = NockStack::new(8 << 10 << 10, 0); /// let encoded = result.to_noun(&mut stack); -/// let decoded = Result::::from_noun(&encoded).unwrap(); +/// let decoded = Result::::from_noun(&encoded, space).unwrap(); /// assert_eq!(result, decoded); /// ``` impl NounEncode for Result { @@ -688,7 +695,7 @@ impl NounEncode for Result { /// 3. Decodes the tail into the appropriate type /// 4. Wraps in Ok/Err accordingly impl NounDecode for Result { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { trace!("\nDecoding Result from noun: {:?}", noun); let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; trace!("Result cell head: {:?}", cell.head()); @@ -705,11 +712,11 @@ impl NounDecode for Result { match tag.as_str() { "ok" => { trace!("Decoding Ok variant"); - Ok(Ok(T::from_noun(&cell.tail())?)) + Ok(Ok(T::from_noun(&cell.tail(), space)?)) } "err" => { trace!("Decoding Err variant"); - Ok(Err(E::from_noun(&cell.tail())?)) + Ok(Err(E::from_noun(&cell.tail(), space)?)) } _ => { trace!("Invalid Result tag: {}", tag); @@ -790,7 +797,7 @@ impl From for NounDecodeError { /// /// let mut stack = NockStack::new(8 << 10 << 10, 0); /// let encoded = set.to_noun(&mut stack); -/// let decoded = HashSet::::from_noun(&encoded).unwrap(); +/// let decoded = HashSet::::from_noun(&encoded, space).unwrap(); /// assert_eq!(set, decoded); /// ``` impl NounEncode for HashSet @@ -849,7 +856,7 @@ impl NounDecode for HashSet where T: std::hash::Hash + Eq, { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // Handle empty tree case if let Ok(atom) = noun.as_atom() { if atom.as_u64()? == 0 { @@ -864,6 +871,7 @@ where fn traverse_tree( node: &Noun, set: &mut HashSet, + space: &NounSpace, ) -> Result<(), NounDecodeError> { // Base case: empty branch if let Ok(atom) = node.as_atom() { @@ -876,7 +884,7 @@ where let cell = node.as_cell()?; // Insert the node value - let value = T::from_noun(&cell.head())?; + let value = T::from_noun(&cell.head(), space)?; set.insert(value); // Get left and right branches @@ -885,13 +893,13 @@ where let right = &rest.tail(); // Recursively process left and right branches - traverse_tree(left, set)?; - traverse_tree(right, set)?; + traverse_tree(left, set, space)?; + traverse_tree(right, set, space)?; Ok(()) } - traverse_tree(noun, &mut set)?; + traverse_tree(noun, &mut set, space)?; Ok(set) } } @@ -923,7 +931,7 @@ where /// /// let mut stack = NockStack::new(8 << 10 << 10, 0); /// let encoded = map.to_noun(&mut stack); -/// let decoded = BTreeMap::::from_noun(&encoded).unwrap(); +/// let decoded = BTreeMap::::from_noun(&encoded, space).unwrap(); /// assert_eq!(map, decoded); /// ``` impl NounEncode for BTreeMap { @@ -992,12 +1000,13 @@ impl NounEncode for BTreeMap { /// 3. Recursively processes all subtrees /// 4. Atom 0 represents empty subtrees impl NounDecode for BTreeMap { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let mut map = BTreeMap::new(); fn traverse_tree( node: &Noun, map: &mut BTreeMap, + space: &NounSpace, ) -> Result<(), NounDecodeError> { // Base case: empty tree (atom 0) if let Ok(atom) = node.as_atom() { @@ -1011,8 +1020,8 @@ impl NounDecode for BTreeMap { // Get the [key value] pair from the node let pair = cell.head().as_cell()?; - let key = K::from_noun(&pair.head())?; - let value = V::from_noun(&pair.tail())?; + let key = K::from_noun(&pair.head(), space)?; + let value = V::from_noun(&pair.tail(), space)?; map.insert(key, value); // Get left and right subtrees @@ -1021,13 +1030,13 @@ impl NounDecode for BTreeMap { let right = &rest.tail(); // Recursively process left and right subtrees - traverse_tree(left, map)?; - traverse_tree(right, map)?; + traverse_tree(left, map, space)?; + traverse_tree(right, map, space)?; Ok(()) } - traverse_tree(noun, &mut map)?; + traverse_tree(noun, &mut map, space)?; Ok(map) } } @@ -1047,8 +1056,8 @@ impl NounEncode for usize { /// usize values are decoded from u64 atoms. On 32-bit systems, values larger /// than u32::MAX will cause a decode error. impl NounDecode for usize { - fn from_noun(noun: &Noun) -> Result { - let value = u64::from_noun(noun)?; + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + let value = u64::from_noun(noun, space)?; // Check if the value fits in usize for the current architecture if value > usize::MAX as u64 { @@ -1063,7 +1072,7 @@ impl NounDecode for usize { } impl NounDecode for [T; N] { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let mut result = Vec::with_capacity(N); if N > 0 { @@ -1075,13 +1084,13 @@ impl NounDecode for [T; N] { .as_cell() .map_err(|_| NounDecodeError::ExpectedCell)?; let head = cell.head(); - let item = T::from_noun(&head)?; + let item = T::from_noun(&head, space)?; result.push(item); current = cell.tail(); } // Process the last element - let last_item = T::from_noun(¤t)?; + let last_item = T::from_noun(¤t, space)?; result.push(last_item); } @@ -1130,7 +1139,7 @@ mod array_tests { assert_eq!(noun.as_atom().unwrap().as_u64().unwrap(), 0); // Test decoding - let decoded: [u64; 0] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [u64; 0] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded.len(), 0); } @@ -1148,7 +1157,7 @@ mod array_tests { assert_eq!(noun.as_atom().unwrap().as_u64().unwrap(), 42); // Test decoding - let decoded: [u64; 1] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [u64; 1] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded, [42]); } @@ -1188,7 +1197,7 @@ mod array_tests { assert_eq!(tail4.as_atom().unwrap().as_u64().unwrap(), 5); // Test decoding - let decoded: [u64; 5] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [u64; 5] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded, [1, 2, 3, 4, 5]); } @@ -1202,7 +1211,7 @@ mod array_tests { let noun = array.to_noun(stack); // Test decoding - let decoded: [String; 2] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [String; 2] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded, ["hello".to_string(), "world".to_string()]); } #[test] @@ -1215,7 +1224,7 @@ mod array_tests { let noun = array.to_noun(stack); // Test decoding - let decoded: [bool; 3] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [bool; 3] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded, [true, false, true]); } #[test] @@ -1228,7 +1237,7 @@ mod array_tests { let noun = array.to_noun(stack); // Test decoding - let decoded: [[u64; 2]; 2] = NounDecode::from_noun(&noun).unwrap(); + let decoded: [[u64; 2]; 2] = NounDecode::from_noun(&noun, space).unwrap(); assert_eq!(decoded, [[1, 2], [3, 4]]); } @@ -1241,14 +1250,14 @@ mod array_tests { let noun = D(42); // Try to decode as a multi-element array (should fail) - let result = <[u64; 3]>::from_noun(&noun); + let result = <[u64; 3]>::from_noun(&noun, space); assert!(result.is_err()); // Create a cell with wrong structure let bad_noun = T(stack, &[D(1), D(2)]); // Last element should be a cell for a 3-element array - let result = <[u64; 3]>::from_noun(&bad_noun); + let result = <[u64; 3]>::from_noun(&bad_noun, space); assert!(result.is_err()); } } @@ -1294,7 +1303,7 @@ mod btreemap_tests { assert_eq!(noun.as_atom().unwrap().as_u64().unwrap(), 0); // Round-trip test - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); assert_eq!(map, decoded); } @@ -1310,7 +1319,7 @@ mod btreemap_tests { assert!(noun.as_cell().is_ok()); // Round-trip test - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); assert_eq!(map, decoded); assert_eq!(decoded.get(&42), Some(&"hello".to_string())); } @@ -1328,7 +1337,7 @@ mod btreemap_tests { let noun = map.to_noun(&mut stack); // Round-trip test - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); assert_eq!(map, decoded); // Check all values are preserved @@ -1351,7 +1360,7 @@ mod btreemap_tests { let noun = map.to_noun(&mut stack); // Round-trip test - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); assert_eq!(map, decoded); // Check specific values @@ -1380,7 +1389,7 @@ mod btreemap_tests { let noun = outer.to_noun(&mut stack); // Round-trip test - let decoded = BTreeMap::>::from_noun(&noun).unwrap(); + let decoded = BTreeMap::>::from_noun(&noun, space).unwrap(); assert_eq!(outer, decoded); // Check nested values @@ -1396,7 +1405,7 @@ mod btreemap_tests { // Test with invalid atom (not 0) let invalid_atom = nockvm::noun::Atom::new(&mut stack, 42).as_noun(); - let result = BTreeMap::::from_noun(&invalid_atom); + let result = BTreeMap::::from_noun(&invalid_atom, space); assert!(result.is_err()); // Test with malformed cell (missing key-value structure) @@ -1408,7 +1417,7 @@ mod btreemap_tests { nockvm::noun::D(0), ], ); - let result = BTreeMap::::from_noun(&malformed); + let result = BTreeMap::::from_noun(&malformed, space); assert!(result.is_err()); } @@ -1424,7 +1433,7 @@ mod btreemap_tests { } let noun = map.to_noun(&mut stack); - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); // BTreeMap should maintain sorted order let original_keys: Vec<_> = map.keys().collect(); @@ -1447,7 +1456,7 @@ mod btreemap_tests { for &value in &values { let noun = value.to_noun(&mut stack); - let decoded = usize::from_noun(&noun).unwrap(); + let decoded = usize::from_noun(&noun, space).unwrap(); assert_eq!(value, decoded); } } @@ -1463,7 +1472,7 @@ mod btreemap_tests { let noun = map.to_noun(&mut stack); // Round-trip test - let decoded = BTreeMap::::from_noun(&noun).unwrap(); + let decoded = BTreeMap::::from_noun(&noun, space).unwrap(); assert_eq!(map, decoded); // Check specific values diff --git a/nockchain-minimal/crates/noun-serde/src/wallet.rs b/nockchain-minimal/crates/noun-serde/src/wallet.rs index f986e3c..76ce3a9 100644 --- a/nockchain-minimal/crates/noun-serde/src/wallet.rs +++ b/nockchain-minimal/crates/noun-serde/src/wallet.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use nockapp::utils::make_tas; use nockapp::AtomExt; -use nockvm::noun::{Noun, NounAllocator, D, T}; +use nockvm::noun::{Noun, NounAllocator, NounSpace, D, T}; use crate::{NounDecode, NounDecodeError, NounEncode}; @@ -32,7 +32,7 @@ impl NounEncode for Key { impl NounDecode for Key { #[allow(unused_variables)] - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let tag = cell.head().as_atom()?.into_string()?; let value = cell.tail().as_atom()?.as_u64()?; @@ -66,7 +66,7 @@ impl NounEncode for Coil { } impl NounDecode for Coil { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let tag = cell.head().as_atom()?.into_string()?; if tag != "coil" { @@ -74,7 +74,7 @@ impl NounDecode for Coil { } let data = cell.tail().as_cell()?; - let key = Key::from_noun(&data.head())?; + let key = Key::from_noun(&data.head(), space)?; let knot = data.tail().as_atom()?.as_u64()?; Ok(Coil { key, knot }) @@ -108,12 +108,12 @@ impl NounEncode for Meta { } impl NounDecode for Meta { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let tag = cell.head().as_atom()?.into_string()?; match tag.as_str() { - "coil" => Ok(Meta::Coil(Coil::from_noun(noun)?)), + "coil" => Ok(Meta::Coil(Coil::from_noun(noun, space)?)), "label" => { let value = cell.tail().as_atom()?.into_string()?; Ok(Meta::Label(value)) @@ -153,13 +153,13 @@ impl NounEncode for Transaction { } impl NounDecode for Transaction { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let recipient = cell.head().as_atom()?.as_u64()?; let tail = cell.tail().as_cell()?; let amount = tail.head().as_atom()?.as_u64()?; - let status = TransactionStatus::from_noun(&tail.tail())?; + let status = TransactionStatus::from_noun(&tail.tail(), space)?; Ok(Transaction { recipient, @@ -181,7 +181,7 @@ impl NounEncode for TransactionStatus { } impl NounDecode for TransactionStatus { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let tag = noun.as_atom()?.into_string()?; match tag.as_str() { "unsigned" => Ok(TransactionStatus::Unsigned), @@ -222,7 +222,7 @@ impl NounEncode for FileEffect { } impl NounDecode for FileEffect { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let file_tag = cell.head().as_atom()?.into_string()?; if file_tag != "file" { @@ -274,12 +274,12 @@ impl NounEncode for Effect { } impl NounDecode for Effect { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let tag = cell.head().as_atom()?.into_string()?; match tag.as_str() { - "file" => Ok(Effect::File(FileEffect::from_noun(noun)?)), + "file" => Ok(Effect::File(FileEffect::from_noun(noun, space)?)), "markdown" => { let text = cell.tail().as_atom()?.into_string()?; Ok(Effect::Markdown(text)) @@ -322,7 +322,7 @@ impl NounEncode for SpendMask { } impl NounDecode for SpendMask { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let signature = cell.head().as_atom()?.as_u64()? != 0; @@ -363,10 +363,10 @@ impl NounEncode for InputMask { } impl NounDecode for InputMask { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let note = cell.head().as_atom()?.as_u64()? != 0; - let spend = SpendMask::from_noun(&cell.tail())?; + let spend = SpendMask::from_noun(&cell.tail(), space)?; Ok(InputMask { note, spend }) } } @@ -414,7 +414,7 @@ impl NounEncode for SeedMask { } impl NounDecode for SeedMask { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let mut current = noun; let next_cell = |n: &Noun| -> Result<(Noun, Noun), NounDecodeError> { let cell = n.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; @@ -460,13 +460,13 @@ impl NounEncode for PreSeed { } impl NounDecode for PreSeed { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let name = cell.head().as_atom()?.into_string()?; let data = cell.tail().as_cell()?; let seed = data.head().as_atom()?.as_u64()?; - let mask = SeedMask::from_noun(&data.tail())?; + let mask = SeedMask::from_noun(&data.tail(), space)?; Ok(PreSeed { name, seed, mask }) } @@ -491,13 +491,13 @@ impl NounEncode for PreInput { } impl NounDecode for PreInput { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let name = cell.head().as_atom()?.into_string()?; let data = cell.tail().as_cell()?; let input = data.head().as_atom()?.as_u64()?; - let mask = InputMask::from_noun(&data.tail())?; + let mask = InputMask::from_noun(&data.tail(), space)?; Ok(PreInput { name, input, mask }) } @@ -519,7 +519,7 @@ impl NounEncode for Draft { } impl NounDecode for Draft { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let name = cell.head().as_atom()?.into_string()?; let inputs = cell.tail().as_atom()?.as_u64()?; @@ -570,7 +570,7 @@ impl NounEncode for DraftEntity { } impl NounDecode for DraftEntity { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let tag = cell.head().as_atom()?.into_string()?; @@ -579,15 +579,15 @@ impl NounDecode for DraftEntity { let kind = match tag.as_str() { "draft" => { - let draft = Draft::from_noun(&data.tail())?; + let draft = Draft::from_noun(&data.tail(), space)?; DraftEntityKind::Draft { name, draft } } "input" => { - let input = PreInput::from_noun(&data.tail())?; + let input = PreInput::from_noun(&data.tail(), space)?; DraftEntityKind::Input { name, input } } "seed" => { - let seed = PreSeed::from_noun(&data.tail())?; + let seed = PreSeed::from_noun(&data.tail(), space)?; DraftEntityKind::Seed { name, seed } } _ => return Err(NounDecodeError::InvalidEnumVariant), @@ -613,10 +613,10 @@ impl NounEncode for Master { } impl NounDecode for Master { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; - let pub_key = Coil::from_noun(&cell.head())?; - let prv_key = Coil::from_noun(&cell.tail())?; + let pub_key = Coil::from_noun(&cell.head(), space)?; + let prv_key = Coil::from_noun(&cell.tail(), space)?; Ok(Master { pub_key, prv_key }) } @@ -644,8 +644,8 @@ impl NounEncode for Balance { } impl NounDecode for Balance { - fn from_noun(noun: &Noun) -> Result { - let notes = HashMap::from_noun(noun)?; + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + let notes = HashMap::from_noun(noun, space)?; Ok(Balance { notes }) } } @@ -668,7 +668,7 @@ impl NounEncode for Network { } impl NounDecode for Network { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let tag = noun.as_atom()?.into_string()?; match tag.as_str() { "mainnet" => Ok(Network::Mainnet), @@ -696,7 +696,7 @@ impl NounEncode for PeekRequest { } impl NounDecode for PeekRequest { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let tag = noun.as_atom()?.into_string()?; match tag.as_str() { "balance" => Ok(PeekRequest::Balance), @@ -808,7 +808,7 @@ impl NounEncode for Trek { } impl NounDecode for Trek { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let mut current = noun.clone(); let mut parts = Vec::new(); while let Ok(cell) = current.as_cell() { @@ -837,7 +837,7 @@ impl NounEncode for Source { } impl NounDecode for Source { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let head = cell.head().as_atom()?.as_u64()?; let tail = cell.tail().as_atom()?.as_u64()?; @@ -876,10 +876,10 @@ impl NounEncode for Lock { } impl NounDecode for Lock { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let m = cell.head().as_atom()?.as_u64()?; - let pubkeys = HashSet::from_noun(&cell.tail())?; + let pubkeys = HashSet::from_noun(&cell.tail(), space)?; Ok(Lock { m, pubkeys }) } } @@ -910,7 +910,7 @@ impl NounEncode for Timelock { } impl NounDecode for Timelock { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, _space: &NounSpace) -> Result { let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; let block = cell.head().as_atom()?.as_u64()?; let intent = match cell.tail().as_atom()?.as_u64()? { @@ -979,7 +979,7 @@ impl NounEncode for Seed { } impl NounDecode for Seed { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { println!("\nDecoding Seed from noun: {:?}", noun); let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; @@ -1015,7 +1015,7 @@ impl NounDecode for Seed { } println!("Decoding Some(Source)"); - Some(Source::from_noun(&source_cell.tail())?) + Some(Source::from_noun(&source_cell.tail(), space)?) }; println!("Decoded output_source: {:?}", output_source); @@ -1026,7 +1026,7 @@ impl NounDecode for Seed { rest.tail() ); - let recipient = Lock::from_noun(&rest.head())?; + let recipient = Lock::from_noun(&rest.head(), space)?; println!("Decoded recipient: {:?}", recipient); let rest = rest.tail().as_cell()?; @@ -1113,7 +1113,7 @@ impl NounEncode for Spend { } impl NounDecode for Spend { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { println!("\nDecoding Spend from noun: {:?}", noun); let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; @@ -1149,7 +1149,7 @@ impl NounDecode for Spend { } println!("Decoding Some(HashMap)"); - Some(HashMap::from_noun(&sig_cell.tail())?) + Some(HashMap::from_noun(&sig_cell.tail(), space)?) }; println!("Decoded signature: {:?}", signature); @@ -1160,7 +1160,7 @@ impl NounDecode for Spend { data.tail() ); - let seeds = HashSet::from_noun(&data.head())?; + let seeds = HashSet::from_noun(&data.head(), space)?; println!("Decoded seeds: {:?}", seeds); let fee = data.tail().as_atom()?.as_u64()?; @@ -1193,7 +1193,7 @@ mod tests { "key".to_string(), ]); let encoded = trek.to_noun(&mut stack); - let decoded = Trek::from_noun(&encoded).unwrap(); + let decoded = Trek::from_noun(&encoded, space).unwrap(); assert_eq!(trek, decoded); } @@ -1203,12 +1203,12 @@ mod tests { let hash = Source::Hash(0x1234); let encoded = hash.to_noun(&mut stack); - let decoded = Source::from_noun(&encoded).unwrap(); + let decoded = Source::from_noun(&encoded, space).unwrap(); assert_eq!(hash, decoded); let coinbase = Source::Coinbase; let encoded = coinbase.to_noun(&mut stack); - let decoded = Source::from_noun(&encoded).unwrap(); + let decoded = Source::from_noun(&encoded, space).unwrap(); assert_eq!(coinbase, decoded); } @@ -1222,7 +1222,7 @@ mod tests { let lock = Lock { m: 2, pubkeys }; let encoded = lock.to_noun(&mut stack); - let decoded = Lock::from_noun(&encoded).unwrap(); + let decoded = Lock::from_noun(&encoded, space).unwrap(); assert_eq!(lock, decoded); } @@ -1235,7 +1235,7 @@ mod tests { intent: TimelockIntent::After, }; let encoded = timelock.to_noun(&mut stack); - let decoded = Timelock::from_noun(&encoded).unwrap(); + let decoded = Timelock::from_noun(&encoded, space).unwrap(); assert_eq!(timelock, decoded); } @@ -1254,7 +1254,7 @@ mod tests { parent_hash: 0x9abc, }; let encoded = seed.to_noun(&mut stack); - let decoded = Seed::from_noun(&encoded).unwrap(); + let decoded = Seed::from_noun(&encoded, space).unwrap(); assert_eq!(seed, decoded); } @@ -1274,7 +1274,7 @@ mod tests { }, }; let encoded = preseed.to_noun(&mut stack); - let decoded = PreSeed::from_noun(&encoded).unwrap(); + let decoded = PreSeed::from_noun(&encoded, space).unwrap(); assert_eq!(preseed, decoded); } @@ -1304,7 +1304,7 @@ mod tests { fee: 10, }; let encoded = spend.to_noun(&mut stack); - let decoded = Spend::from_noun(&encoded).unwrap(); + let decoded = Spend::from_noun(&encoded, space).unwrap(); assert_eq!(spend, decoded); } @@ -1325,7 +1325,7 @@ mod tests { }, }; let encoded = preinput.to_noun(&mut stack); - let decoded = PreInput::from_noun(&encoded).unwrap(); + let decoded = PreInput::from_noun(&encoded, space).unwrap(); assert_eq!(preinput, decoded); } @@ -1338,7 +1338,7 @@ mod tests { inputs: 0x1234, // Using u64 as specified in struct }; let encoded = draft.to_noun(&mut stack); - let decoded = Draft::from_noun(&encoded).unwrap(); + let decoded = Draft::from_noun(&encoded, space).unwrap(); assert_eq!(draft, decoded); } } diff --git a/nockchain-minimal/crates/zkvm-jetpack/src/jets/mary_jets.rs b/nockchain-minimal/crates/zkvm-jetpack/src/jets/mary_jets.rs index b2d6f9c..b34d8ea 100644 --- a/nockchain-minimal/crates/zkvm-jetpack/src/jets/mary_jets.rs +++ b/nockchain-minimal/crates/zkvm-jetpack/src/jets/mary_jets.rs @@ -6,7 +6,7 @@ use nockvm::jets::math::util::add; use nockvm::jets::util::{bite_to_word, chop, slot}; use nockvm::jets::JetErr; use nockvm::mem::NockStack; -use nockvm::noun::{Atom, IndirectAtom, Noun, D, NO, T, YES}; +use nockvm::noun::{Atom, IndirectAtom, Noun, NounAllocator, D, NO, T, YES}; use nockvm_macros::tas; use tracing::{debug, error}; @@ -324,7 +324,8 @@ fn heapify_mary(stack: &mut NockStack, m_noun: Noun) -> Result { for i in 0..ma_array_len.as_u64()? { let t = snag_as_bpoly(stack, m_noun, i as usize)?; let hashable_bpoly = T(stack, &[D(tas!(b"mary")), D(1), t]); - let hash = hash_hashable(stack, hashable_bpoly)?; + let space = stack.noun_space(); + let hash = hash_hashable(stack, hashable_bpoly, &space)?; let leafs = leaf_sequence(stack, hash)?; res_vec.push(leafs); } diff --git a/nockchain-minimal/crates/zkvm-jetpack/src/jets/tip5_jets.rs b/nockchain-minimal/crates/zkvm-jetpack/src/jets/tip5_jets.rs index b46ffd5..510bcf0 100644 --- a/nockchain-minimal/crates/zkvm-jetpack/src/jets/tip5_jets.rs +++ b/nockchain-minimal/crates/zkvm-jetpack/src/jets/tip5_jets.rs @@ -4,7 +4,7 @@ use nockvm::jets::list::util::{lent, weld}; use nockvm::jets::util::slot; use nockvm::jets::JetErr; use nockvm::mem::NockStack; -use nockvm::noun::{Atom, Noun, D, T}; +use nockvm::noun::{Atom, Noun, NounAllocator, NounSpace, D, T}; use nockvm_macros::tas; use crate::based; @@ -357,13 +357,14 @@ fn hash_noun_varlen(stack: &mut NockStack, n: Noun) -> Result { } pub fn hash_hashable_jet(context: &mut Context, subject: Noun) -> Result { + let space = context.stack.noun_space(); let stack = &mut context.stack; let h = slot(subject, 6)?; - hash_hashable(stack, h) + hash_hashable(stack, h, &space) } -pub fn hash_hashable(stack: &mut NockStack, h: Noun) -> Result { +pub fn hash_hashable(stack: &mut NockStack, h: Noun, space: &NounSpace) -> Result { if !h.is_cell() { return jet_err(); } @@ -375,32 +376,48 @@ pub fn hash_hashable(stack: &mut NockStack, h: Noun) -> Result { let tag = h_head.as_direct()?; match tag.data() { - tas!(b"hash") => hash_hashable_hash(stack, h_tail), - tas!(b"leaf") => hash_hashable_leaf(stack, h_tail), - tas!(b"list") => hash_hashable_list(stack, h_tail), - tas!(b"mary") => hash_hashable_mary(stack, h_tail), - _ => hash_hashable_other(stack, h_head, h_tail), + tas!(b"hash") => hash_hashable_hash(stack, h_tail, space), + tas!(b"leaf") => hash_hashable_leaf(stack, h_tail, space), + tas!(b"list") => hash_hashable_list(stack, h_tail, space), + tas!(b"mary") => hash_hashable_mary(stack, h_tail, space), + _ => hash_hashable_other(stack, h_head, h_tail, space), } } else { - hash_hashable_other(stack, h_head, h_tail) + hash_hashable_other(stack, h_head, h_tail, space) } } -fn hash_hashable_hash(_stack: &mut NockStack, p: Noun) -> Result { +fn hash_hashable_hash( + _stack: &mut NockStack, + p: Noun, + _space: &NounSpace, +) -> Result { Ok(p) } -fn hash_hashable_leaf(stack: &mut NockStack, p: Noun) -> Result { +fn hash_hashable_leaf( + stack: &mut NockStack, + p: Noun, + _space: &NounSpace, +) -> Result { hash_noun_varlen(stack, p) } -fn hash_hashable_list(stack: &mut NockStack, p: Noun) -> Result { +fn hash_hashable_list( + stack: &mut NockStack, + p: Noun, + space: &NounSpace, +) -> Result { let turn: Vec = HoonList::try_from(p)? .into_iter() - .map(|x| hash_hashable(stack, x).unwrap()) + .map(|x| hash_hashable(stack, x, space).unwrap()) .collect(); let turn_list = vecnoun_to_hoon_list(stack, &turn); hash_noun_varlen(stack, turn_list) } -fn hash_hashable_mary(stack: &mut NockStack, p: Noun) -> Result { +fn hash_hashable_mary( + stack: &mut NockStack, + p: Noun, + space: &NounSpace, +) -> Result { let (ma_step, ma_array_len, _ma_array_dat) = get_mary_fields(p)?; let ma_changed = change_step(stack, p, D(1))?; @@ -413,12 +430,17 @@ fn hash_hashable_mary(stack: &mut NockStack, p: Noun) -> Result { let hash = T(stack, &[D(tas!(b"hash")), hash_belts_list]); let arg = T(stack, &[leaf_step, leaf_len, hash]); - hash_hashable(stack, arg) + hash_hashable(stack, arg, space) } -fn hash_hashable_other(stack: &mut NockStack, p: Noun, q: Noun) -> Result { - let ph = hash_hashable(stack, p)?; - let qh = hash_hashable(stack, q)?; +fn hash_hashable_other( + stack: &mut NockStack, + p: Noun, + q: Noun, + space: &NounSpace, +) -> Result { + let ph = hash_hashable(stack, p, space)?; + let qh = hash_hashable(stack, q, space)?; let cell = T(stack, &[ph, qh]); diff --git a/pokenoun/src/arena.rs b/pokenoun/src/arena.rs index be5a16e..ede5fb8 100644 --- a/pokenoun/src/arena.rs +++ b/pokenoun/src/arena.rs @@ -33,6 +33,15 @@ pub struct Arena { cells: Vec, } +#[inline] +fn trim_trailing_zeros(bytes: &[u8]) -> &[u8] { + let mut len = bytes.len(); + while len > 0 && bytes[len - 1] == 0 { + len -= 1; + } + &bytes[..len] +} + impl Arena { pub fn new() -> Self { // AtomId(0) is always the canonical `0` atom with an empty byte slice. @@ -58,9 +67,19 @@ impl Arena { &self.atom_data[start..end] } + /// Atom bytes with trailing zero bytes removed. + /// + /// Jam permits non-minimal atom encodings (for example `%pkh` with a + /// trailing zero byte). Numeric comparisons must treat those as the same + /// atom, and `atom_u64`/`atom_eq_bytes` do exactly that. + #[inline] + pub fn atom_bytes_trimmed(&self, id: AtomId) -> &[u8] { + trim_trailing_zeros(self.atom_bytes(id)) + } + #[inline] pub fn atom_u64(&self, id: AtomId) -> Option { - let bytes = self.atom_bytes(id); + let bytes = self.atom_bytes_trimmed(id); if bytes.len() > 8 { return None; } @@ -73,7 +92,7 @@ impl Arena { #[inline] pub fn atom_eq_bytes(&self, id: AtomId, bytes: &[u8]) -> bool { - self.atom_bytes(id) == bytes + self.atom_bytes_trimmed(id) == trim_trailing_zeros(bytes) } pub fn alloc_atom_bytes(&mut self, bytes: &[u8]) -> Noun { @@ -128,3 +147,47 @@ impl Arena { } } +#[cfg(test)] +mod tests { + use super::*; + + fn non_minimal_atom(arena: &mut Arena, bytes: &[u8], extra_zero_bytes: usize) -> Noun { + let off = arena.atom_data.len(); + arena.atom_data.extend_from_slice(bytes); + arena + .atom_data + .extend(core::iter::repeat(0u8).take(extra_zero_bytes)); + let len = bytes.len() + extra_zero_bytes; + let id = arena.push_atom_slice(off as u32, len as u32); + Noun::Atom(id) + } + + #[test] + fn atom_eq_bytes_ignores_trailing_zero_bytes() { + let mut arena = Arena::new(); + let minimal = arena.alloc_atom_bytes(b"pkh"); + let padded = non_minimal_atom(&mut arena, b"pkh", 2); + + let Noun::Atom(padded_id) = padded else { + panic!("expected atom"); + }; + let Noun::Atom(minimal_id) = minimal else { + panic!("expected atom"); + }; + + assert_eq!(arena.atom_u64(padded_id), arena.atom_u64(minimal_id)); + assert!(arena.atom_eq_bytes(padded_id, b"pkh")); + // `%pkh` encoded with a trailing zero byte still matches. + assert!(arena.atom_eq_bytes(minimal_id, b"pkh\0\0")); + } + + #[test] + fn atom_u64_trims_non_minimal_encodings() { + let mut arena = Arena::new(); + let padded = non_minimal_atom(&mut arena, &[1, 2], 3); + let Noun::Atom(id) = padded else { + panic!("expected atom"); + }; + assert_eq!(arena.atom_u64(id), Some(0x0201)); + } +} diff --git a/pokenoun/src/jam.rs b/pokenoun/src/jam.rs index 0bf5a7b..68a8232 100644 --- a/pokenoun/src/jam.rs +++ b/pokenoun/src/jam.rs @@ -26,6 +26,12 @@ impl<'a> BitReader<'a> { self.bit as u64 } + /// Number of unread bits remaining in the input. + fn remaining_bits(&self) -> u64 { + let total = (self.bytes.len() as u64).saturating_mul(8); + total.saturating_sub(self.bit as u64) + } + fn read_bit(&mut self) -> Result { let byte_i = self.bit / 8; if byte_i >= self.bytes.len() { @@ -51,8 +57,8 @@ impl<'a> BitReader<'a> { } } -fn get_size(reader: &mut BitReader<'_>) -> Result { - let mut zeros = 0usize; +fn get_size(reader: &mut BitReader<'_>) -> Result { + let mut zeros = 0u32; loop { let b = reader.read_bit()?; if b { @@ -66,9 +72,14 @@ fn get_size(reader: &mut BitReader<'_>) -> Result { if zeros == 0 { return Ok(0); } - let bitsize = zeros; - let low = reader.read_bits_u64(bitsize.saturating_sub(1))? as usize; - Ok(low + (1usize << (bitsize - 1))) + let low = reader.read_bits_u64((zeros - 1) as usize)?; + // `zeros` is at most 64, so `zeros - 1` is at most 63 and the shift is + // well-defined for u64 on every target (including 32-bit ESP32 where a + // `usize` shift of 63 would overflow). + let high = 1u64 + .checked_shl(zeros - 1) + .ok_or(CodecError::AtomTooLarge)?; + high.checked_add(low).ok_or(CodecError::AtomTooLarge) } fn rub_backref(reader: &mut BitReader<'_>) -> Result { @@ -79,7 +90,7 @@ fn rub_backref(reader: &mut BitReader<'_>) -> Result { if size > 64 { return Err(CodecError::InvalidBackref); } - reader.read_bits_u64(size) + reader.read_bits_u64(size as usize) } fn met0_u64(x: u64) -> usize { @@ -120,12 +131,27 @@ pub fn cue(bytes: &[u8], arena: &mut Arena) -> Result { let noun = if size == 0 { arena.atom0() } else { + // Never allocate based on an unchecked size header: the atom + // bits must actually be present in the remaining input. + if size > reader.remaining_bits() { + return Err(CodecError::UnexpectedEof); + } let nbytes = (size + 7) / 8; + let nbytes = usize::try_from(nbytes).map_err(|_| CodecError::AtomTooLarge)?; let off = arena.atom_data.len(); - arena.atom_data.resize(off + nbytes, 0u8); + let new_len = off.checked_add(nbytes).ok_or(CodecError::AtomTooLarge)?; + // `size <= remaining_bits` bounds the allocation by the input + // size; `try_reserve` still reports allocation failure instead + // of aborting on tiny targets. + arena + .atom_data + .try_reserve(nbytes) + .map_err(|_| CodecError::AtomTooLarge)?; + arena.atom_data.resize(new_len, 0u8); for i in 0..size { if reader.read_bit()? { - arena.atom_data[off + (i / 8)] |= 1u8 << (i % 8); + let byte_i = (i / 8) as usize; + arena.atom_data[off + byte_i] |= 1u8 << (i % 8); } } let id = arena.push_atom_slice(off as u32, nbytes as u32); @@ -278,3 +304,92 @@ pub fn jam(noun: Noun, arena: &Arena) -> Vec { writer.bytes } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cue_rejects_empty_input_without_panicking() { + let mut arena = Arena::new(); + assert!(matches!( + cue(&[], &mut arena), + Err(CodecError::UnexpectedEof) + )); + } + + #[test] + fn cue_rejects_size_larger_than_remaining_bits() { + // Atom tag (0), then a size header claiming 1024 atom bits, then EOF. + let mut writer = BitWriter::new(); + writer.write_bit(false); // atom + for _ in 0..11 { + writer.write_bit(false); // met0(1024) == 11 + } + writer.write_bit(true); + for _ in 0..10 { + writer.write_bit(false); // low size bits + } + + let mut arena = Arena::new(); + assert!(matches!( + cue(&writer.bytes, &mut arena), + Err(CodecError::UnexpectedEof) + )); + } + + #[test] + fn cue_rejects_oversized_atom_header_before_allocating() { + // Header claims ~2^32 bits; the decoder must reject it before any + // multi-gigabyte allocation. + let mut writer = BitWriter::new(); + writer.write_bit(false); + for _ in 0..33 { + writer.write_bit(false); + } + writer.write_bit(true); + + let mut arena = Arena::new(); + let err = cue(&writer.bytes, &mut arena).unwrap_err(); + assert!(matches!( + err, + CodecError::UnexpectedEof | CodecError::AtomTooLarge + )); + assert!(arena.atom_data.len() < 1024); + } + + #[test] + fn cue_round_trips_atoms_and_cells() { + let mut arena = Arena::new(); + let atom = arena.alloc_atom_u64(42); + let bytes_atom = arena.alloc_atom_bytes(b"hello"); + let cell = arena.alloc_cell(atom, bytes_atom); + let encoded = jam(cell, &arena); + + let mut decoded_arena = Arena::new(); + let decoded = cue(&encoded, &mut decoded_arena).expect("valid jam must cue"); + let Noun::Cell(id) = decoded else { + panic!("expected cell"); + }; + let cell = decoded_arena.cell(id); + assert_eq!( + decoded_arena.atom_u64(match cell.head { + Noun::Atom(a) => a, + _ => panic!("expected atom head"), + }), + Some(42) + ); + } + + #[test] + fn shared_dag_walk_is_bounded() { + // A cell whose tail is a backreference to itself: legal jam, but its + // logical expansion is infinite. The walk must return an error. + let encoded = [0xf1, 0x01]; + let mut arena = Arena::new(); + let noun = cue(&encoded, &mut arena).expect("cyclic jam is decodable"); + assert!(matches!( + crate::tip5::hash_noun_varlen(noun, &arena), + Err(crate::tip5::Tip5Error::WalkTooLarge) + )); + } +} diff --git a/pokenoun/src/tip5.rs b/pokenoun/src/tip5.rs index 4c9e8d7..38d2199 100644 --- a/pokenoun/src/tip5.rs +++ b/pokenoun/src/tip5.rs @@ -9,6 +9,10 @@ pub const GOLDILOCKS_P: u64 = 0xffff_ffff_0000_0001; const PRIME_128: u128 = 18446744069414584321; const STATE_SIZE: usize = 16; const RATE: usize = 10; +/// Maximum number of logical nodes visited by the leaf/dyck walks. A small jam +/// can contain a shared-cell DAG whose logical expansion is exponential; the +/// budget keeps hostile jams from executing forever. +const WALK_MAX_NODES: usize = 1 << 20; const DIGEST_LENGTH: usize = 5; const NUM_ROUNDS: usize = 7; const NUM_SPLIT_AND_LOOKUP: usize = 4; @@ -23,6 +27,8 @@ const RP: u128 = 0xffff_ffff_0000_0001_0000_0000_0000_0000; pub enum Tip5Error { NotBased, BadLength, + /// The logical noun walk exceeded its node budget (hostile shared DAG). + WalkTooLarge, } const LOOKUP_TABLE: [u8; 256] = [ @@ -373,22 +379,8 @@ fn create_init_sponge_variable() -> [u64; STATE_SIZE] { fn create_init_sponge_fixed() -> [u64; STATE_SIZE] { [ - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - 0u64, - R_MOD_P, - R_MOD_P, - R_MOD_P, - R_MOD_P, - R_MOD_P, - R_MOD_P, + 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, 0u64, R_MOD_P, R_MOD_P, R_MOD_P, + R_MOD_P, R_MOD_P, R_MOD_P, ] } @@ -476,7 +468,12 @@ pub fn hash_ten_cell(left: [u64; 5], right: [u64; 5]) -> Result<[u64; 5], Tip5Er fn leaf_sequence(noun: Noun, arena: &Arena, out: &mut Vec) -> Result<(), Tip5Error> { let mut stack: Vec = Vec::new(); stack.push(noun); + let mut visited = 0usize; while let Some(n) = stack.pop() { + visited += 1; + if visited > WALK_MAX_NODES { + return Err(Tip5Error::WalkTooLarge); + } match n { Noun::Atom(id) => { let Some(v) = arena.atom_u64(id) else { @@ -506,7 +503,12 @@ fn dyck_sequence(noun: Noun, arena: &Arena, out: &mut Vec) -> Result<(), Ti let mut stack: Vec = Vec::new(); stack.push(Frame::Node(noun)); + let mut visited = 0usize; while let Some(frame) = stack.pop() { + visited += 1; + if visited > WALK_MAX_NODES { + return Err(Tip5Error::WalkTooLarge); + } match frame { Frame::Node(n) => match n { Noun::Atom(_) => {} @@ -545,3 +547,35 @@ pub fn hash_noun_varlen(noun: Noun, arena: &Arena) -> Result<[u64; 5], Tip5Error hash_varlen_words(&transcript) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn walk_budget_rejects_cyclic_noun() { + // Locally built cyclic noun: a cell with itself as tail. + let mut arena = Arena::new(); + let atom = arena.alloc_atom_u64(1); + let zero = arena.atom0(); + let cell = arena.alloc_cell(atom, zero); + let Noun::Cell(id) = cell else { + panic!("expected cell"); + }; + arena.cell_mut(id).tail = cell; + + assert!(matches!( + hash_noun_varlen(cell, &arena), + Err(Tip5Error::WalkTooLarge) + )); + } + + #[test] + fn unbased_atom_is_rejected() { + let mut arena = Arena::new(); + let atom = arena.alloc_atom_bytes(&[1; 9]); + assert!(matches!( + hash_noun_varlen(atom, &arena), + Err(Tip5Error::NotBased) + )); + } +} diff --git a/pokenoun/src/zmap.rs b/pokenoun/src/zmap.rs index 4b030a8..c92b4c6 100644 --- a/pokenoun/src/zmap.rs +++ b/pokenoun/src/zmap.rs @@ -19,11 +19,16 @@ fn noun_is_null(noun: Noun, arena: &Arena) -> bool { fn nouns_equal(a: Noun, b: Noun, arena: &Arena) -> bool { match (a, b) { - (Noun::Atom(x), Noun::Atom(y)) => arena.atom_bytes(x) == arena.atom_bytes(y), + // Compare atoms by numeric value: jam allows non-minimal encodings + // (e.g. a trailing zero byte), and those must not split z-map keys. + (Noun::Atom(x), Noun::Atom(y)) => { + arena.atom_bytes_trimmed(x) == arena.atom_bytes_trimmed(y) + } (Noun::Cell(ac), Noun::Cell(bc)) => { let a_cell = arena.cell(ac); let b_cell = arena.cell(bc); - nouns_equal(a_cell.head, b_cell.head, arena) && nouns_equal(a_cell.tail, b_cell.tail, arena) + nouns_equal(a_cell.head, b_cell.head, arena) + && nouns_equal(a_cell.tail, b_cell.tail, arena) } _ => false, } @@ -33,8 +38,8 @@ fn atom_less_than(a: Noun, b: Noun, arena: &Arena) -> bool { let (Noun::Atom(a), Noun::Atom(b)) = (a, b) else { return false; }; - let ab = arena.atom_bytes(a); - let bb = arena.atom_bytes(b); + let ab = arena.atom_bytes_trimmed(a); + let bb = arena.atom_bytes_trimmed(b); if ab.len() != bb.len() { return ab.len() < bb.len(); } @@ -200,3 +205,51 @@ pub fn canonical_zmap_put( } } +#[cfg(test)] +mod tests { + use super::*; + + fn non_minimal_atom(arena: &mut Arena, bytes: &[u8]) -> Noun { + let off = arena.atom_data.len(); + arena.atom_data.extend_from_slice(bytes); + arena.atom_data.push(0); + let id = arena.push_atom_slice(off as u32, (bytes.len() + 1) as u32); + Noun::Atom(id) + } + + #[test] + fn nouns_equal_treats_non_minimal_atoms_as_equal() { + let mut arena = Arena::new(); + let minimal = arena.alloc_atom_bytes(b"pkh"); + let padded = non_minimal_atom(&mut arena, b"pkh"); + + assert!(nouns_equal(minimal, padded, &arena)); + } + + #[test] + fn canonical_zmap_put_does_not_split_equivalent_key_spellings() { + let mut arena = Arena::new(); + let key = arena.alloc_atom_bytes(b"lock"); + let other_key = non_minimal_atom(&mut arena, b"lock"); + let value_a = arena.alloc_atom_u64(1); + let value_b = arena.alloc_atom_u64(2); + + let empty = arena.atom0(); + let map = canonical_zmap_put(&mut arena, empty, key, value_a).unwrap(); + let map = canonical_zmap_put(&mut arena, map, other_key, value_b).unwrap(); + + // Same key slot (and balanced children) means the value was replaced, + // not inserted as a second entry. + let (node, left, right) = decompose_map(map, &arena).unwrap(); + let (_, value) = decompose_pair(node, &arena).unwrap(); + assert_eq!( + arena.atom_u64(match value { + Noun::Atom(id) => id, + _ => panic!("expected atom value"), + }), + Some(2) + ); + assert!(noun_is_null(left, &arena)); + assert!(noun_is_null(right, &arena)); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index d3e25b1..2417807 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly-2025-02-14" +channel = "nightly-2026-04-03" diff --git a/tx-types/Cargo.toml b/tx-types/Cargo.toml index 475445a..7bdf212 100644 --- a/tx-types/Cargo.toml +++ b/tx-types/Cargo.toml @@ -12,17 +12,36 @@ license = "MIT OR Apache-2.0" hmac = { workspace = true } sha2 = { workspace = true } zeroize = { workspace = true } -ibig = { workspace = true, default-features = false } pokenoun = { workspace = true } +# The Nockchain runtime is selected with the `embedded` / `native` features. +# Both dependency sets are declared here and aliased onto the canonical crate +# names by `lib.rs`, so the same source compiles against either one. +nockapp-minimal = { package = "nockapp", path = "../nockchain-minimal/crates/nockapp", default-features = false, optional = true } +nockapp-upstream = { package = "nockapp", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false, optional = true } + +nockvm-minimal = { package = "nockvm", path = "../nockchain-minimal/crates/nockvm/rust/nockvm", default-features = false, features = ["malloc"], optional = true } +nockvm-upstream = { package = "nockvm", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", optional = true } + +noun-serde-minimal = { package = "noun-serde", path = "../nockchain-minimal/crates/noun-serde", optional = true } +noun-serde-upstream = { package = "noun-serde", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", optional = true } + +nockchain-math-minimal = { package = "nockchain-math", path = "../nockchain-minimal/crates/nockchain-math", optional = true } +nockchain-math-upstream = { package = "nockchain-math", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", optional = true } + +zkvm-jetpack-minimal = { package = "zkvm-jetpack", path = "../nockchain-minimal/crates/zkvm-jetpack", optional = true } +zkvm-jetpack-upstream = { package = "zkvm-jetpack", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", optional = true } + +nockchain-libp2p-io-minimal = { package = "nockchain-libp2p-io", path = "../nockchain-minimal/crates/nockchain-libp2p-io", default-features = false, optional = true } +nockchain-libp2p-io-upstream = { package = "nockchain-libp2p-io", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false, optional = true } + +nockvm_macros-minimal = { package = "nockvm_macros", path = "../nockchain-minimal/crates/nockvm/rust/nockvm_macros", optional = true } +nockvm_macros-upstream = { package = "nockvm_macros", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", optional = true } + +ibig-minimal = { package = "ibig", path = "../nockchain-minimal/crates/nockvm/rust/ibig", default-features = false } +ibig-upstream = { package = "ibig", git = "https://github.com/SWPSCO/nockchain.git", rev = "73877be29bd1fafeeade1be7b95aebb0f64d8382", default-features = false, optional = true } + # Optional std dependencies -noun-serde = { workspace = true, optional = true } -nockapp = { workspace = true, optional = true } -nockvm = { workspace = true, optional = true } -nockvm_macros = { workspace = true, optional = true } -nockchain-libp2p-io = { workspace = true, optional = true } -zkvm-jetpack = { workspace = true, optional = true } -nockchain-math = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } serde_json = { workspace = true, optional = true } either = { workspace = true, optional = true } @@ -35,19 +54,47 @@ bip39 = { workspace = true, optional = true, features = ["zeroize"] } pbkdf2 = { workspace = true, optional = true } bytes = { workspace = true, optional = true } once_cell = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } + +[[bin]] +name = "sign_raw_tx" +path = "src/bin/sign_raw_tx.rs" +required-features = ["std"] + +[[bin]] +name = "zkey_pub_b58" +path = "src/bin/zkey_pub_b58.rs" +required-features = ["std"] [features] -default = ["std"] +default = ["embedded", "std"] + +embedded = [ + "dep:nockapp-minimal", + "dep:nockvm-minimal", + "dep:noun-serde-minimal", + "dep:nockchain-math-minimal", + "dep:zkvm-jetpack-minimal", + "dep:nockchain-libp2p-io-minimal", + "dep:nockvm_macros-minimal", + "ibig-minimal/std", + "ibig-minimal/num-traits", +] + +native = [ + "dep:nockapp-upstream", + "dep:nockvm-upstream", + "dep:noun-serde-upstream", + "dep:nockchain-math-upstream", + "dep:zkvm-jetpack-upstream", + "dep:nockchain-libp2p-io-upstream", + "dep:nockvm_macros-upstream", + "dep:ibig-upstream", + "ibig-upstream/std", + "ibig-upstream/num-traits", +] + std = [ - "ibig/std", - "ibig/num-traits", - "dep:noun-serde", - "dep:nockapp", - "dep:nockvm", - "dep:nockvm_macros", - "dep:nockchain-libp2p-io", - "dep:zkvm-jetpack", - "dep:nockchain-math", "dep:serde", "dep:serde_json", "dep:either", @@ -61,6 +108,7 @@ std = [ "dep:pbkdf2", "dep:bytes", "dep:once_cell", + "dep:tracing", ] tracy = [] legacy-tests = [] diff --git a/tx-types/src/bin/sign_raw_tx.rs b/tx-types/src/bin/sign_raw_tx.rs index 00ac53b..202c09d 100644 --- a/tx-types/src/bin/sign_raw_tx.rs +++ b/tx-types/src/bin/sign_raw_tx.rs @@ -1,8 +1,24 @@ +#[cfg(feature = "embedded")] +extern crate nockapp_minimal as nockapp; +#[cfg(feature = "embedded")] +extern crate nockvm_minimal as nockvm; +#[cfg(feature = "embedded")] +extern crate noun_serde_minimal as noun_serde; + +#[cfg(feature = "native")] +extern crate nockapp_upstream as nockapp; +#[cfg(feature = "native")] +extern crate nockvm_upstream as nockvm; +#[cfg(feature = "native")] +extern crate noun_serde_upstream as noun_serde; + #[cfg(feature = "std")] use bytes::Bytes; #[cfg(feature = "std")] use nockapp::noun::slab::{NockJammer, NounSlab}; #[cfg(feature = "std")] +use nockvm::noun::NounAllocator; +#[cfg(feature = "std")] use noun_serde::{NounDecode, NounEncode}; #[cfg(feature = "std")] use std::io::{Read, Write}; @@ -119,11 +135,20 @@ fn main() { std::process::exit(1); }); - let (out_bytes, report_lines) = if let Ok(mut raw_tx) = RawTransactionV1::from_noun(&noun) { + let space = slab.noun_space(); + let (out_bytes, report_lines) = if let Ok(mut raw_tx) = + RawTransactionV1::from_noun(&noun, &space) + { let (signed_spends, spends_signed) = sign_spends_v1(raw_tx.spends, &signing_key, &schnorr_pubkey, &pubkey_hash); raw_tx.spends = signed_spends; - let tx_id_hash = compute_tx_id_v1(&raw_tx.spends); + let tx_id_hash = match compute_tx_id_v1(&raw_tx.spends) { + Ok(hash) => hash, + Err(err) => { + eprintln!("error: failed to compute tx id: {err}"); + std::process::exit(1); + } + }; raw_tx.id = tx_id_hash.clone(); let out_bytes = jam_to_vec(&raw_tx); @@ -134,10 +159,16 @@ fn main() { format!("spends_signed: {}", spends_signed), ]; (out_bytes, report) - } else if let Ok(spends) = SpendsV1::from_noun(&noun) { + } else if let Ok(spends) = SpendsV1::from_noun(&noun, &space) { let (signed_spends, spends_signed) = sign_spends_v1(spends, &signing_key, &schnorr_pubkey, &pubkey_hash); - let tx_id_hash = compute_tx_id_v1(&signed_spends); + let tx_id_hash = match compute_tx_id_v1(&signed_spends) { + Ok(hash) => hash, + Err(err) => { + eprintln!("error: failed to compute tx id: {err}"); + std::process::exit(1); + } + }; let out_bytes = jam_to_vec(&signed_spends); let report = vec![ @@ -147,7 +178,7 @@ fn main() { format!("spends_signed: {}", spends_signed), ]; (out_bytes, report) - } else if let Ok(mut spend) = Spend::from_noun(&noun) { + } else if let Ok(mut spend) = Spend::from_noun(&noun, &space) { let spend_signed = sign_spend_in_place(&mut spend, &signing_key, &schnorr_pubkey, &pubkey_hash); let out_bytes = jam_to_vec(&spend); @@ -211,7 +242,10 @@ fn sign_spend_in_place( return false; } - let sig_hash = spend.body.sig_hash(); + let sig_hash = spend + .body + .sig_hash() + .expect("spend signature hashable is u64-based"); let (chal, sig) = schnorr_sign_digest(signing_key.clone(), schnorr_pubkey.clone(), sig_hash); let schnorr_sig = SchnorrSignature { chal: Chal { values: chal }, diff --git a/tx-types/src/block_types.rs b/tx-types/src/block_types.rs index e594e6d..70d597d 100644 --- a/tx-types/src/block_types.rs +++ b/tx-types/src/block_types.rs @@ -1,5 +1,6 @@ use crate::collections::zmap::ZMap; use crate::collections::zset::ZSet; +use crate::fee_calculator::BYTHOS_PHASE; use crate::hashing::hashable::Hashable; use crate::hashing::hasher::{hash_hashable, hash_ten_cell}; use crate::transaction_types::*; @@ -8,7 +9,7 @@ use crate::transaction_types_v1::*; use bytes::Bytes; use chrono::{DateTime, TimeZone, Utc}; use nockapp::noun::slab::NounSlab; -use nockvm::noun::Noun; +use nockvm::noun::{Noun, NounSpace}; use noun_serde::{NounDecode, NounDecodeError, NounEncode}; use num_bigint::BigUint; use serde::{Deserialize, Serialize}; @@ -36,7 +37,7 @@ impl Coinbase { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("coinbase hashable is u64-based") } } @@ -69,7 +70,7 @@ pub fn make_name(pkh_hashes: ZSet, parent_hash: Hash) -> NName { let lk: SpendCondition = SpendCondition { p: vec![pkh, tim] }; - let lmp = build_lock_merkle_proof(lk, 1); + let lmp = build_lock_merkle_proof(lk, 1, BYTHOS_PHASE); let root = lmp.proof().root.clone(); NName::new_v1( @@ -81,8 +82,22 @@ pub fn make_name(pkh_hashes: ZSet, parent_hash: Hash) -> NName { ) } -pub fn build_lock_merkle_proof(form: SpendCondition, leaf_number: u64) -> LockMerkleProof { - build_lock_merkle_proof_full(form, leaf_number) +/// Build a lock merkle proof appropriate for the given page. +/// +/// Hoon rejects a `%full` proof before Bythos activation +/// (`check-context.check: (gte now bythos-phase)`), so choose the stub form +/// below `BYTHOS_PHASE` and the full form at/after it. Callers that know the +/// current chain height should pass it here. +pub fn build_lock_merkle_proof( + form: SpendCondition, + leaf_number: u64, + page: u64, +) -> LockMerkleProof { + if page >= BYTHOS_PHASE { + build_lock_merkle_proof_full(form, leaf_number) + } else { + build_lock_merkle_proof_stub(form, leaf_number) + } } pub fn build_lock_merkle_proof_stub(form: SpendCondition, leaf_number: u64) -> LockMerkleProof { @@ -145,7 +160,7 @@ fn prove_hashable_go(node: &Hashable, index: u64) -> (Hash, Vec, u64) { match node { // Base case: non-cell node (Leaf, Hash, or List) Hashable::Leaf(_) | Hashable::Hash(_) | Hashable::List(_) => { - let root = hash_hashable(node); + let root = hash_hashable(node).expect("lock hashable is u64-based"); (root, Vec::new(), 1) } @@ -156,7 +171,7 @@ fn prove_hashable_go(node: &Hashable, index: u64) -> (Hash, Vec, u64) { if index <= lc { // The target leaf is in the left subtree let (rec_root, mut rec_path, rec_axis) = prove_hashable_go(left, index); - let sibling = hash_hashable(right); + let sibling = hash_hashable(right).expect("lock hashable is u64-based"); // Combine the recursive root with the sibling let root = hash_ten_cell(rec_root, sibling.clone()); @@ -171,7 +186,7 @@ fn prove_hashable_go(node: &Hashable, index: u64) -> (Hash, Vec, u64) { } else { // The target leaf is in the right subtree let (rec_root, mut rec_path, rec_axis) = prove_hashable_go(right, index - lc); - let sibling = hash_hashable(left); + let sibling = hash_hashable(left).expect("lock hashable is u64-based"); // Combine sibling with the recursive root let root = hash_ten_cell(sibling.clone(), rec_root); @@ -230,7 +245,7 @@ impl TransactionIds { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("coinbase hashable is u64-based") } } @@ -250,7 +265,7 @@ impl EpochCounter { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("coinbase hashable is u64-based") } } @@ -280,7 +295,7 @@ impl PageMsg { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("coinbase hashable is u64-based") } } @@ -333,11 +348,11 @@ pub enum Page { } impl NounDecode for Page { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = PageV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = PageV0::from_noun(noun, space) { return Ok(Page::V0(v0)); } - if let Ok(v1) = PageV1::from_noun(noun) { + if let Ok(v1) = PageV1::from_noun(noun, space) { return Ok(Page::V1(v1)); } Err(NounDecodeError::Custom( @@ -374,17 +389,19 @@ pub struct Pow { } impl NounDecode for Pow { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { if noun.is_atom() { return Ok(Pow { p: None }); } let proof = noun + .in_space(space) .as_cell() .map_err(|_| NounDecodeError::ExpectedCell)? - .tail(); + .tail() + .noun(); let mut slab: NounSlab = NounSlab::new(); - slab.copy_into(proof); + slab.copy_into(proof, space); let noun_bytes: Bytes = slab.jam(); Ok(Pow { p: Some(noun_bytes), @@ -440,9 +457,9 @@ impl Timestamp { } impl NounDecode for Timestamp { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let base_urbit_epoch = 0x8000000cce9e0d80u64; - let raw_value = u64::from_noun(noun)?; + let raw_value = u64::from_noun(noun, space)?; let unix_timestamp = (raw_value - base_urbit_epoch) as i64; let datetime_utc = Utc .timestamp_opt(unix_timestamp, 0) @@ -565,6 +582,7 @@ impl PageV0 { /// This is the hash of the block commitment structure (everything after PoW) pub fn hash_block_commitment(&self) -> Hash { hash_hashable(&self.to_hashable_block_commitment()) + .expect("block-commitment hashable is u64-based") } } diff --git a/tx-types/src/collections/zmap.rs b/tx-types/src/collections/zmap.rs index 4423ffa..d92318d 100644 --- a/tx-types/src/collections/zmap.rs +++ b/tx-types/src/collections/zmap.rs @@ -1,3 +1,4 @@ +use crate::hashing::tip5::Tip5Error; use crate::hashing::tip5::Tip5Hasher; use ibig::UBig; use nockvm::noun::D; @@ -60,57 +61,71 @@ where ZMap { root: None } } - /// Insert a key-value pair, maintaining treap invariants + /// Insert a key-value pair, maintaining treap invariants. + /// + /// Panics if the key's noun cannot be TIP5-hashed (for example an atom + /// that does not fit in a belt). Use [`Self::try_put`] to handle that. pub fn put(&mut self, key: K, value: V) { - self.root = Self::put_recursive(self.root.take(), key, value); + self.try_put(key, value) + .unwrap_or_else(|e| panic!("z-map key could not be tip5-hashed: {}", e)); + } + + /// Fallible insertion; see [`Self::put`]. + pub fn try_put(&mut self, key: K, value: V) -> Result<(), Tip5Error> { + self.root = Self::put_recursive(self.root.take(), key, value)?; + Ok(()) } /// Recursive insertion with rotations - fn put_recursive(node: Option>>, key: K, value: V) -> Option>> { + fn put_recursive( + node: Option>>, + key: K, + value: V, + ) -> Result>>, Tip5Error> { match node { None => { // Empty node, create new - Some(Box::new(Node::new(key, value))) + Ok(Some(Box::new(Node::new(key, value)))) } Some(mut n) => { - let cmp = Self::gor_tip(&key, &n.key); + let cmp = Self::gor_tip(&key, &n.key)?; match cmp { Ordering::Equal => { // Key exists, update value n.value = value; - Some(n) + Ok(Some(n)) } Ordering::Less => { // Insert left - n.left = Self::put_recursive(n.left, key, value); + n.left = Self::put_recursive(n.left, key, value)?; // Check if rotation needed (mor-tip comparison) - if let Some(ref left_child) = n.left { - if Self::mor_tip(&n.key, &left_child.key) != Ordering::Less { + if let Some(left_child) = &n.left { + if Self::mor_tip(&n.key, &left_child.key)? != Ordering::Less { // Left child has higher priority, rotate right - Some(Self::rotate_right(n)) + Ok(Some(Self::rotate_right(n))) } else { - Some(n) + Ok(Some(n)) } } else { - Some(n) + Ok(Some(n)) } } Ordering::Greater => { // Insert right - n.right = Self::put_recursive(n.right, key, value); + n.right = Self::put_recursive(n.right, key, value)?; // Check if rotation needed (mor-tip comparison) - if let Some(ref right_child) = n.right { - if Self::mor_tip(&n.key, &right_child.key) != Ordering::Less { + if let Some(right_child) = &n.right { + if Self::mor_tip(&n.key, &right_child.key)? != Ordering::Less { // Right child has higher priority, rotate left - Some(Self::rotate_left(n)) + Ok(Some(Self::rotate_left(n))) } else { - Some(n) + Ok(Some(n)) } } else { - Some(n) + Ok(Some(n)) } } } @@ -154,48 +169,82 @@ where right } - /// Get a value by key + /// Get a value by key. + /// + /// Panics if the key's noun cannot be TIP5-hashed. Use [`Self::try_get`] + /// to handle that. pub fn get(&self, key: &K) -> Option<&V> { + self.try_get(key) + .unwrap_or_else(|e| panic!("z-map key could not be tip5-hashed: {}", e)) + } + + /// Fallible lookup; see [`Self::get`]. + pub fn try_get(&self, key: &K) -> Result, Tip5Error> { Self::get_recursive(&self.root, key) } - fn get_recursive<'a>(node: &'a Option>>, key: &K) -> Option<&'a V> { + fn get_recursive<'a>( + node: &'a Option>>, + key: &K, + ) -> Result, Tip5Error> { match node { - None => None, - Some(n) => match Self::gor_tip(key, &n.key) { - Ordering::Equal => Some(&n.value), + None => Ok(None), + Some(n) => match Self::gor_tip(key, &n.key)? { + Ordering::Equal => Ok(Some(&n.value)), Ordering::Less => Self::get_recursive(&n.left, key), Ordering::Greater => Self::get_recursive(&n.right, key), }, } } - /// Check if a key exists + /// Check if a key exists. + /// + /// Panics if the key's noun cannot be TIP5-hashed. Use [`Self::try_has`] + /// to handle that. pub fn has(&self, key: &K) -> bool { - self.get(key).is_some() + self.try_has(key) + .unwrap_or_else(|e| panic!("z-map key could not be tip5-hashed: {}", e)) + } + + /// Fallible membership check; see [`Self::has`]. + pub fn try_has(&self, key: &K) -> Result { + Ok(self.try_get(key)?.is_some()) } - /// Delete a key from the tree + /// Delete a key from the tree. + /// + /// Panics if the key's noun cannot be TIP5-hashed. Use [`Self::try_del`] + /// to handle that. pub fn del(&mut self, key: &K) { - self.root = Self::del_recursive(self.root.take(), key); + self.try_del(key) + .unwrap_or_else(|e| panic!("z-map key could not be tip5-hashed: {}", e)); + } + + /// Fallible deletion; see [`Self::del`]. + pub fn try_del(&mut self, key: &K) -> Result<(), Tip5Error> { + self.root = Self::del_recursive(self.root.take(), key)?; + Ok(()) } - fn del_recursive(node: Option>>, key: &K) -> Option>> { + fn del_recursive( + node: Option>>, + key: &K, + ) -> Result>>, Tip5Error> { match node { - None => None, + None => Ok(None), Some(mut n) => { - match Self::gor_tip(key, &n.key) { + match Self::gor_tip(key, &n.key)? { Ordering::Equal => { // Found node to delete, merge children Self::merge_children(n.left, n.right) } Ordering::Less => { - n.left = Self::del_recursive(n.left, key); - Some(n) + n.left = Self::del_recursive(n.left, key)?; + Ok(Some(n)) } Ordering::Greater => { - n.right = Self::del_recursive(n.right, key); - Some(n) + n.right = Self::del_recursive(n.right, key)?; + Ok(Some(n)) } } } @@ -206,21 +255,21 @@ where fn merge_children( left: Option>>, right: Option>>, - ) -> Option>> { + ) -> Result>>, Tip5Error> { match (left, right) { - (None, None) => None, - (Some(l), None) => Some(l), - (None, Some(r)) => Some(r), + (None, None) => Ok(None), + (Some(l), None) => Ok(Some(l)), + (None, Some(r)) => Ok(Some(r)), (Some(mut l), Some(mut r)) => { // Choose root based on mor-tip priority - if Self::mor_tip(&l.key, &r.key) == Ordering::Less { + if Self::mor_tip(&l.key, &r.key)? == Ordering::Less { // l has higher priority (smaller mor-tip), make it root - l.right = Self::merge_children(l.right, Some(r)); - Some(l) + l.right = Self::merge_children(l.right, Some(r))?; + Ok(Some(l)) } else { // r has higher priority, make it root - r.left = Self::merge_children(Some(l), r.left); - Some(r) + r.left = Self::merge_children(Some(l), r.left)?; + Ok(Some(r)) } } } @@ -254,65 +303,64 @@ where } /// Compute TIP5 hash of a key and convert to UBig integer - /// Returns a UBig representing the hash for comparison - fn compute_tip5_hash(key: &K) -> UBig { - use crate::transaction_types::Hash; + /// Returns a UBig representing the hash for comparison. + fn compute_tip5_hash(key: &K) -> Result { use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + // Create a noun slab and convert key to noun let mut slab: NounSlab = NounSlab::new(); let key_noun = key.to_noun(&mut slab); + let space = slab.noun_space(); // Compute the TIP5 hash - let hash = - Tip5Hasher::hash_noun_varlen(key_noun).unwrap_or_else(|_| Hash { values: [0; 5] }); - - // Convert Hash to UBig using our new method - hash.to_ubig() + Tip5Hasher::hash_noun_varlen(key_noun, &space).map(|hash| hash.to_ubig()) } /// gor-tip: Compare by single hash (UBig integer), fallback to dor-tip - fn gor_tip(a: &K, b: &K) -> Ordering { - let hash_a = Self::compute_tip5_hash(a); - let hash_b = Self::compute_tip5_hash(b); + fn gor_tip(a: &K, b: &K) -> Result { + let hash_a = Self::compute_tip5_hash(a)?; + let hash_b = Self::compute_tip5_hash(b)?; // Compare the UBig integers - match hash_a.cmp(&hash_b) { + Ok(match hash_a.cmp(&hash_b) { Ordering::Equal => a.dor_tip(b), other => other, - } + }) } /// mor-tip: Compare by double hash (hash of hash concatenated with itself), fallback to dor-tip - fn mor_tip(a: &K, b: &K) -> Ordering { + fn mor_tip(a: &K, b: &K) -> Result { // Compute double-tip for each value - let double_a = Self::compute_double_tip5_hash(a); - let double_b = Self::compute_double_tip5_hash(b); + let double_a = Self::compute_double_tip5_hash(a)?; + let double_b = Self::compute_double_tip5_hash(b)?; - match double_a.cmp(&double_b) { + Ok(match double_a.cmp(&double_b) { Ordering::Equal => a.dor_tip(b), other => other, - } + }) } /// Compute double-tip: hash(hash(value) ++ hash(value)) - fn compute_double_tip5_hash(key: &K) -> UBig { + fn compute_double_tip5_hash(key: &K) -> Result { use crate::hashing::hasher::hash_ten_cell; - use crate::transaction_types::Hash; use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + // First compute the regular hash let mut slab: NounSlab = NounSlab::new(); let key_noun = key.to_noun(&mut slab); + let space = slab.noun_space(); - let hash = - Tip5Hasher::hash_noun_varlen(key_noun).unwrap_or_else(|_| Hash { values: [0; 5] }); + let hash = Tip5Hasher::hash_noun_varlen(key_noun, &space)?; // Use hash_ten_cell with two copies of the hash let double_hash = hash_ten_cell(hash.clone(), hash); // Convert to UBig - double_hash.to_ubig() + Ok(double_hash.to_ubig()) } /// Build from an iterator (gas equivalent) @@ -342,6 +390,52 @@ where } } + /// Fallible variant of [`Self::to_hashable`] for keys or values whose + /// hashable construction can fail (for example note-data with unbased + /// atoms). + pub fn try_to_hashable( + &self, + key_fn: F, + val_fn: G, + ) -> Result + where + F: Fn(&K) -> Result + Copy, + G: Fn(&V) -> Result + Copy, + { + use crate::hashing::hashable::Hashable; + + match &self.root { + None => Ok(Hashable::null()), + Some(node) => Self::try_node_to_hashable(node, key_fn, val_fn), + } + } + + fn try_node_to_hashable( + node: &Box>, + key_fn: F, + val_fn: G, + ) -> Result + where + F: Fn(&K) -> Result + Copy, + G: Fn(&V) -> Result + Copy, + { + use crate::hashing::hashable::Hashable; + + let node_pair = Hashable::cell(key_fn(&node.key)?, val_fn(&node.value)?); + + let left = match &node.left { + None => Hashable::null(), + Some(left_node) => Self::try_node_to_hashable(left_node, key_fn, val_fn)?, + }; + + let right = match &node.right { + None => Hashable::null(), + Some(right_node) => Self::try_node_to_hashable(right_node, key_fn, val_fn)?, + }; + + Ok(Hashable::triple(node_pair, left, right)) + } + /// Convert a node to hashable recursively fn node_to_hashable( node: &Box>, @@ -2234,22 +2328,26 @@ where K: noun_serde::NounDecode + DorTip + Clone + NounEncode + Debug, V: noun_serde::NounDecode + Clone + Debug, { - fn from_noun(noun: &nockvm::noun::Noun) -> Result { - use nockvm::noun::Noun; - + fn from_noun( + noun: &nockvm::noun::Noun, + space: &nockvm::noun::NounSpace, + ) -> Result { // Check if it's ~ (empty map) - if noun.is_atom() && noun.as_atom().unwrap().as_u64()? == 0 { - return Ok(ZMap::new()); + let handle = noun.in_space(space); + if let Ok(atom) = handle.as_atom() { + if atom.as_u64()? == 0 { + return Ok(ZMap::new()); + } } // Otherwise it should be a cell [[key value] [left right]] - let cell = noun + handle .as_cell() .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; // Decode the node recursively let mut map = ZMap::new(); - Self::decode_node_recursive(noun, &mut map)?; + Self::decode_node_recursive(noun, space, &mut map)?; Ok(map) } } @@ -2261,15 +2359,20 @@ where { fn decode_node_recursive( noun: &nockvm::noun::Noun, + space: &nockvm::noun::NounSpace, map: &mut ZMap, ) -> Result<(), noun_serde::NounDecodeError> { + let handle = noun.in_space(space); + // If it's ~ (0), nothing to do - if noun.is_atom() && noun.as_atom().unwrap().as_u64()? == 0 { - return Ok(()); + if let Ok(atom) = handle.as_atom() { + if atom.as_u64()? == 0 { + return Ok(()); + } } // Otherwise it's [[key value] [left right]] - let cell = noun + let cell = handle .as_cell() .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; @@ -2279,8 +2382,8 @@ where .as_cell() .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; - let key = K::from_noun(&pair_cell.head())?; - let value = V::from_noun(&pair_cell.tail())?; + let key = K::from_noun(&pair_cell.head().noun(), space)?; + let value = V::from_noun(&pair_cell.tail().noun(), space)?; // Insert the key-value pair map.put(key, value); @@ -2292,8 +2395,8 @@ where .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; // Recursively decode left and right subtrees - Self::decode_node_recursive(&children_cell.head(), map)?; - Self::decode_node_recursive(&children_cell.tail(), map)?; + Self::decode_node_recursive(&children_cell.head().noun(), space, map)?; + Self::decode_node_recursive(&children_cell.tail().noun(), space, map)?; Ok(()) } diff --git a/tx-types/src/collections/zset.rs b/tx-types/src/collections/zset.rs index 8588494..d72ebe5 100644 --- a/tx-types/src/collections/zset.rs +++ b/tx-types/src/collections/zset.rs @@ -1,4 +1,4 @@ -use crate::hashing::tip5::Tip5Hasher; +use crate::hashing::tip5::{Tip5Error, Tip5Hasher}; use ibig::UBig; use nockvm::noun::D; use noun_serde::NounEncode; @@ -57,9 +57,19 @@ where ZSet { root: None } } - /// Insert a value, maintaining treap invariants + /// Insert a value, maintaining treap invariants. + /// + /// Panics if the value's noun cannot be TIP5-hashed. Use [`Self::try_put`] + /// to handle that. pub fn put(&mut self, value: T) { - self.root = Self::put_recursive(self.root.take(), value); + self.try_put(value) + .unwrap_or_else(|e| panic!("z-set value could not be tip5-hashed: {}", e)); + } + + /// Fallible insertion; see [`Self::put`]. + pub fn try_put(&mut self, value: T) -> Result<(), Tip5Error> { + self.root = Self::put_recursive(self.root.take(), value)?; + Ok(()) } /// Insert a value with debug output @@ -131,7 +141,8 @@ where Some(Box::new(Node::new(value))) } Some(mut n) => { - let cmp = Self::gor_tip(&value, &n.value); + let cmp = + Self::gor_tip(&value, &n.value).expect("z-set value could not be tip5-hashed"); println!( "{}Comparing {:?} with node {:?}: {:?}", indent, value, n.value, cmp @@ -146,8 +157,9 @@ where println!("{}Going left", indent); n.left = Self::put_recursive_debug(n.left, value, depth + 1); - if let Some(ref left_child) = n.left { - let mor_cmp = Self::mor_tip(&n.value, &left_child.value); + if let Some(left_child) = &n.left { + let mor_cmp = Self::mor_tip(&n.value, &left_child.value) + .expect("z-set value could not be tip5-hashed"); println!( "{}mor_tip({:?}, {:?}) = {:?}", indent, n.value, left_child.value, mor_cmp @@ -167,8 +179,9 @@ where println!("{}Going right", indent); n.right = Self::put_recursive_debug(n.right, value, depth + 1); - if let Some(ref right_child) = n.right { - let mor_cmp = Self::mor_tip(&n.value, &right_child.value); + if let Some(right_child) = &n.right { + let mor_cmp = Self::mor_tip(&n.value, &right_child.value) + .expect("z-set value could not be tip5-hashed"); println!( "{}mor_tip({:?}, {:?}) = {:?}", indent, n.value, right_child.value, mor_cmp @@ -190,54 +203,57 @@ where } /// Recursive insertion with rotations - fn put_recursive(node: Option>>, value: T) -> Option>> { + fn put_recursive( + node: Option>>, + value: T, + ) -> Result>>, Tip5Error> { match node { None => { // Empty node, create new - Some(Box::new(Node::new(value))) + Ok(Some(Box::new(Node::new(value)))) } Some(mut n) => { - let cmp = Self::gor_tip(&value, &n.value); + let cmp = Self::gor_tip(&value, &n.value)?; match cmp { Ordering::Equal => { // Value exists, no-op for set - Some(n) + Ok(Some(n)) } Ordering::Less => { // Insert left - n.left = Self::put_recursive(n.left, value); + n.left = Self::put_recursive(n.left, value)?; // Check if rotation needed (mor-tip comparison) // In Hoon: if (mor-tip n.a n.c) is true, NO rotation // mor-tip returns Less when first has higher priority (smaller mor value) - if let Some(ref left_child) = n.left { - if Self::mor_tip(&n.value, &left_child.value) != Ordering::Less { + if let Some(left_child) = &n.left { + if Self::mor_tip(&n.value, &left_child.value)? != Ordering::Less { // Parent does NOT have higher priority, rotate right - Some(Self::rotate_right(n)) + Ok(Some(Self::rotate_right(n))) } else { - Some(n) + Ok(Some(n)) } } else { - Some(n) + Ok(Some(n)) } } Ordering::Greater => { // Insert right - n.right = Self::put_recursive(n.right, value); + n.right = Self::put_recursive(n.right, value)?; // Check if rotation needed (mor-tip comparison) // In Hoon: if (mor-tip n.a n.c) is true, NO rotation // mor-tip returns Less when first has higher priority (smaller mor value) - if let Some(ref right_child) = n.right { - if Self::mor_tip(&n.value, &right_child.value) != Ordering::Less { + if let Some(right_child) = &n.right { + if Self::mor_tip(&n.value, &right_child.value)? != Ordering::Less { // Parent does NOT have higher priority, rotate left - Some(Self::rotate_left(n)) + Ok(Some(Self::rotate_left(n))) } else { - Some(n) + Ok(Some(n)) } } else { - Some(n) + Ok(Some(n)) } } } @@ -267,43 +283,65 @@ where right } - /// Check if a value exists + /// Check if a value exists. + /// + /// Panics if the value's noun cannot be TIP5-hashed. Use [`Self::try_has`] + /// to handle that. pub fn has(&self, value: &T) -> bool { + self.try_has(value) + .unwrap_or_else(|e| panic!("z-set value could not be tip5-hashed: {}", e)) + } + + /// Fallible membership check; see [`Self::has`]. + pub fn try_has(&self, value: &T) -> Result { Self::has_recursive(&self.root, value) } - fn has_recursive(node: &Option>>, value: &T) -> bool { + fn has_recursive(node: &Option>>, value: &T) -> Result { match node { - None => false, - Some(n) => match Self::gor_tip(value, &n.value) { - Ordering::Equal => true, + None => Ok(false), + Some(n) => match Self::gor_tip(value, &n.value)? { + Ordering::Equal => Ok(true), Ordering::Less => Self::has_recursive(&n.left, value), Ordering::Greater => Self::has_recursive(&n.right, value), }, } } - /// Delete a value from the tree + /// Delete a value from the tree. + /// + /// Panics if the value's noun cannot be TIP5-hashed. Use [`Self::try_del`] + /// to handle that. pub fn del(&mut self, value: &T) { - self.root = Self::del_recursive(self.root.take(), value); + self.try_del(value) + .unwrap_or_else(|e| panic!("z-set value could not be tip5-hashed: {}", e)); } - fn del_recursive(node: Option>>, value: &T) -> Option>> { + /// Fallible deletion; see [`Self::del`]. + pub fn try_del(&mut self, value: &T) -> Result<(), Tip5Error> { + self.root = Self::del_recursive(self.root.take(), value)?; + Ok(()) + } + + fn del_recursive( + node: Option>>, + value: &T, + ) -> Result>>, Tip5Error> { match node { - None => None, + None => Ok(None), Some(mut n) => { - match Self::gor_tip(value, &n.value) { + match Self::gor_tip(value, &n.value)? { Ordering::Equal => { // Found node to delete, merge children Self::merge_children(n.left, n.right) } Ordering::Less => { - n.left = Self::del_recursive(n.left, value); - Some(n) + n.left = Self::del_recursive(n.left, value)?; + Ok(Some(n)) } Ordering::Greater => { - n.right = Self::del_recursive(n.right, value); - Some(n) + n.right = Self::del_recursive(n.right, value)?; + Ok(Some(n)) } } } @@ -314,21 +352,21 @@ where fn merge_children( left: Option>>, right: Option>>, - ) -> Option>> { + ) -> Result>>, Tip5Error> { match (left, right) { - (None, None) => None, - (Some(l), None) => Some(l), - (None, Some(r)) => Some(r), + (None, None) => Ok(None), + (Some(l), None) => Ok(Some(l)), + (None, Some(r)) => Ok(Some(r)), (Some(mut l), Some(mut r)) => { // Choose root based on mor-tip priority - if Self::mor_tip(&l.value, &r.value) == Ordering::Less { + if Self::mor_tip(&l.value, &r.value)? == Ordering::Less { // l has higher priority (smaller mor-tip), make it root - l.right = Self::merge_children(l.right, Some(r)); - Some(l) + l.right = Self::merge_children(l.right, Some(r))?; + Ok(Some(l)) } else { // r has higher priority, make it root - r.left = Self::merge_children(Some(l), r.left); - Some(r) + r.left = Self::merge_children(Some(l), r.left)?; + Ok(Some(r)) } } } @@ -415,67 +453,66 @@ where } /// Compute TIP5 hash of a value and convert to UBig integer - fn compute_tip5_hash(value: &T) -> UBig { - use crate::transaction_types::Hash; + fn compute_tip5_hash(value: &T) -> Result { use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + // Create a noun slab and convert value to noun let mut slab: NounSlab = NounSlab::new(); let value_noun = value.to_noun(&mut slab); + let space = slab.noun_space(); // Compute the TIP5 hash - let hash = - Tip5Hasher::hash_noun_varlen(value_noun).unwrap_or_else(|_| Hash { values: [0; 5] }); - - // Convert Hash to UBig using our new method - hash.to_ubig() + Tip5Hasher::hash_noun_varlen(value_noun, &space).map(|hash| hash.to_ubig()) } /// gor-tip: Compare by single hash (UBig integer), fallback to dor-tip - fn gor_tip(a: &T, b: &T) -> Ordering { - let hash_a = Self::compute_tip5_hash(a); - let hash_b = Self::compute_tip5_hash(b); + fn gor_tip(a: &T, b: &T) -> Result { + let hash_a = Self::compute_tip5_hash(a)?; + let hash_b = Self::compute_tip5_hash(b)?; // Compare the UBig integers - ORIGINAL (not reversed) - match hash_a.cmp(&hash_b) { + Ok(match hash_a.cmp(&hash_b) { // Note: a compared to b (original) Ordering::Equal => a.dor_tip(b), other => other, - } + }) } /// mor-tip: Compare by double hash (hash of hash concatenated with itself), fallback to dor-tip - fn mor_tip(a: &T, b: &T) -> Ordering { + fn mor_tip(a: &T, b: &T) -> Result { // Compute double-tip for each value - let double_a = Self::compute_double_tip5_hash(a); - let double_b = Self::compute_double_tip5_hash(b); + let double_a = Self::compute_double_tip5_hash(a)?; + let double_b = Self::compute_double_tip5_hash(b)?; // ORIGINAL (not reversed) - match double_a.cmp(&double_b) { + Ok(match double_a.cmp(&double_b) { // Note: a compared to b (original) Ordering::Equal => a.dor_tip(b), other => other, - } + }) } /// Compute double-tip: hash(hash(value) ++ hash(value)) - fn compute_double_tip5_hash(value: &T) -> UBig { + fn compute_double_tip5_hash(value: &T) -> Result { use crate::hashing::hasher::hash_ten_cell; - use crate::transaction_types::Hash; use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + // First compute the regular hash let mut slab: NounSlab = NounSlab::new(); let value_noun = value.to_noun(&mut slab); + let space = slab.noun_space(); - let hash = - Tip5Hasher::hash_noun_varlen(value_noun).unwrap_or_else(|_| Hash { values: [0; 5] }); + let hash = Tip5Hasher::hash_noun_varlen(value_noun, &space)?; // Use hash_ten_cell with two copies of the hash let double_hash = hash_ten_cell(hash.clone(), hash); // Convert to UBig - double_hash.to_ubig() + Ok(double_hash.to_ubig()) } /// Build from an iterator (gas equivalent) @@ -504,6 +541,44 @@ where } } + /// Fallible variant of [`Self::to_hashable`] for values whose hashable + /// construction can fail (for example note-data with unbased atoms). + pub fn try_to_hashable(&self, val_fn: F) -> Result + where + F: Fn(&T) -> Result + Copy, + { + use crate::hashing::hashable::Hashable; + + match &self.root { + None => Ok(Hashable::null()), + Some(node) => Self::try_node_to_hashable(node, val_fn), + } + } + + fn try_node_to_hashable( + node: &Box>, + val_fn: F, + ) -> Result + where + F: Fn(&T) -> Result + Copy, + { + use crate::hashing::hashable::Hashable; + + let node_hashable = val_fn(&node.value)?; + + let left = match &node.left { + None => Hashable::null(), + Some(left_node) => Self::try_node_to_hashable(left_node, val_fn)?, + }; + + let right = match &node.right { + None => Hashable::null(), + Some(right_node) => Self::try_node_to_hashable(right_node, val_fn)?, + }; + + Ok(Hashable::triple(node_hashable, left, right)) + } + /// Convert a node to hashable recursively fn node_to_hashable(node: &Box>, val_fn: F) -> crate::hashing::hashable::Hashable where @@ -667,7 +742,7 @@ where } // Implement NounDecode for ZSet -use nockvm::noun::{Noun, NounAllocator}; +use nockvm::noun::Noun; use noun_serde::{NounDecode, NounDecodeError}; impl ZSet @@ -675,17 +750,26 @@ where T: NounDecode + NounEncode + DorTip + Clone + Debug + PartialEq, { /// Decode a node from noun recursively for NounDecode - fn node_from_noun(noun: &Noun) -> Result>>, NounDecodeError> { + fn node_from_noun( + noun: &Noun, + space: &nockvm::noun::NounSpace, + ) -> Result>>, NounDecodeError> { + let handle = noun.in_space(space); + // Check if it's empty (~ = 0) - if noun.as_atom().is_ok() && noun.as_atom().unwrap().as_u64().unwrap_or(1) == 0 { - return Ok(None); + if let Ok(atom) = handle.as_atom() { + if atom.as_u64().unwrap_or(1) == 0 { + return Ok(None); + } } // Otherwise it should be a cell [value left right] - let cell = noun.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; + let cell = handle + .as_cell() + .map_err(|_| NounDecodeError::ExpectedCell)?; // First element is the value - let value = T::from_noun(&cell.head())?; + let value = T::from_noun(&cell.head().noun(), space)?; // Second element should be a cell [left right] let tail_cell = cell @@ -694,8 +778,8 @@ where .map_err(|_| NounDecodeError::ExpectedCell)?; // Recursively decode left and right subtrees - let left = Self::node_from_noun(&tail_cell.head())?; - let right = Self::node_from_noun(&tail_cell.tail())?; + let left = Self::node_from_noun(&tail_cell.head().noun(), space)?; + let right = Self::node_from_noun(&tail_cell.tail().noun(), space)?; Ok(Some(Box::new(Node { value, left, right }))) } @@ -705,9 +789,9 @@ impl NounDecode for ZSet where T: NounDecode + NounEncode + DorTip + Clone + Debug + PartialEq, { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &nockvm::noun::NounSpace) -> Result { // Decode from tree structure matching Hoon's z-set - let root = Self::node_from_noun(noun)?; + let root = Self::node_from_noun(noun, space)?; Ok(ZSet { root }) } } @@ -860,9 +944,12 @@ mod tests { println!("\n--- In-order Traversal (based on gor-tip ordering) ---"); for (idx, item) in items.iter().enumerate() { // Compute the hash for each value to understand ordering + use nockvm::noun::NounAllocator; + let mut slab: NounSlab = NounSlab::new(); let noun = item.to_noun(&mut slab); - let hash = Tip5Hasher::hash_noun_varlen(noun).unwrap(); + let space = slab.noun_space(); + let hash = Tip5Hasher::hash_noun_varlen(noun, &space).unwrap(); println!("{}: value={}, hash={:x?}", idx, item.0, &hash.values[0..2]); // Show first 2 values of hash } diff --git a/tx-types/src/fee_calculator.rs b/tx-types/src/fee_calculator.rs index 55bd681..d9dfba7 100644 --- a/tx-types/src/fee_calculator.rs +++ b/tx-types/src/fee_calculator.rs @@ -16,13 +16,14 @@ extern crate alloc; use crate::collections::ZMap; +use crate::generic_noun::UntypedNoun; #[cfg(test)] use crate::hashing::hashable::Hashable; use crate::transaction_types::{Coins, Hash, NName, Spend, SpendBody}; use crate::transaction_types_v0::SpendV0; use crate::transaction_types_v1::{NoteData, SeedV1, SeedsV1, SpendV0ToV1, Witness}; use nockapp::noun::slab::{NockJammer, NounSlab}; -use nockvm::noun::Noun; +use nockvm::noun::{Noun, NounAllocator, NounSpace}; use noun_serde::NounEncode; /// Activation height for the Bythos fee and witness-format upgrade. @@ -128,8 +129,34 @@ fn count_seeds_v1_words(seeds: &SeedsV1) -> u64 { .sum() } +/// Count note-data words exactly as Hoon does: +/// `num-of-leaves:shape (~(rep z-by note-data) |=([[k v] tree] [k v tree]))`. +/// +/// The fold builds `[k1 v1 [k2 v2 ... [kn vn ~]]]`, so each entry contributes +/// one word for its key plus the leaves of its value, and the terminal `~` +/// contributes one more word. Counting the z-map noun itself would add the +/// internal `[node left right]` cells and overcharge by one word per entry. fn count_note_data_words(note_data: &NoteData) -> u64 { - count_leaves_from_encoder(|slab| note_data.to_noun(slab)) + let mut total = 1; // terminal ~ + for (_key, value) in note_data.map.tap() { + total += 1; // key + total += count_untyped_noun_leaves(&value); + } + total +} + +/// Count the leaves of an `UntypedNoun` value. Values stored in note-data are +/// arbitrary nouns, so recurse through cells rather than treating the value as +/// a single atom. A value that cannot be cued is counted as one atom. +fn count_untyped_noun_leaves(value: &UntypedNoun) -> u64 { + let mut slab = NounSlab::::new(); + match slab.cue_into(value.p.clone()) { + Ok(noun) => { + let space = slab.noun_space(); + count_leaves_from_noun(noun, &space) + } + Err(_) => 1, + } } fn count_witness_words(spends: &ZMap) -> u64 { @@ -181,12 +208,14 @@ where { let mut slab = NounSlab::::new(); let noun = build(&mut slab); - count_leaves_from_noun(noun) + let space = slab.noun_space(); + count_leaves_from_noun(noun, &space) } -fn count_leaves_from_noun(noun: Noun) -> u64 { - if let Ok(cell) = noun.as_cell() { - count_leaves_from_noun(cell.head()) + count_leaves_from_noun(cell.tail()) +fn count_leaves_from_noun(noun: Noun, space: &NounSpace) -> u64 { + if let Ok(cell) = noun.in_space(space).as_cell() { + count_leaves_from_noun(cell.head().noun(), space) + + count_leaves_from_noun(cell.tail().noun(), space) } else { 1 } @@ -223,6 +252,34 @@ mod tests { assert_eq!(count_leaves(&nested), 3); } + #[test] + fn test_count_note_data_words_matches_hoon_rep_shape() { + // Empty note-data: just the terminal ~. + let empty = NoteData { map: ZMap::new() }; + assert_eq!(count_note_data_words(&empty), 1); + + // One entry: key + value + terminal ~ = 3 words (not the z-map noun's 4). + let one = note_data(&[("alpha", 1)]); + assert_eq!(count_note_data_words(&one), 3); + + // Two entries: 2 * (key + atom value) + terminal ~ = 5 words + // (the z-map noun would count 7). + let two = note_data(&[("alpha", 1), ("beta", 2)]); + assert_eq!(count_note_data_words(&two), 5); + } + + #[test] + fn test_note_data_words_count_value_cell_leaves() { + // A cell value contributes one word per leaf of the value noun. + let mut map = ZMap::new(); + map.put("cell".to_string(), untyped_cell(1, 2)); + let data = NoteData { map }; + assert_eq!( + count_note_data_words(&data), + 1 /* terminal */ + 1 /* key */ + 2 + ); + } + #[test] fn test_bythos_constants() { assert_eq!(BYTHOS_PHASE, 54_000); @@ -358,7 +415,15 @@ mod tests { fn untyped_atom(value: u64) -> UntypedNoun { let mut slab = NounSlab::::new(); - slab.copy_into(D(value)); + let space = slab.noun_space(); + slab.copy_into(D(value), &space); + UntypedNoun { p: slab.jam() } + } + + fn untyped_cell(head: u64, tail: u64) -> UntypedNoun { + let mut slab = NounSlab::::new(); + let noun = nockvm::noun::T(&mut slab, &[D(head), D(tail)]); + slab.set_root(noun); UntypedNoun { p: slab.jam() } } } diff --git a/tx-types/src/generic_noun.rs b/tx-types/src/generic_noun.rs index b8d572d..72b3f2a 100644 --- a/tx-types/src/generic_noun.rs +++ b/tx-types/src/generic_noun.rs @@ -1,6 +1,6 @@ use bytes::Bytes; use nockapp::noun::slab::NounSlab; -use nockvm::noun::{Noun, NounAllocator}; +use nockvm::noun::{Noun, NounAllocator, NounSpace}; use noun_serde::{NounDecode, NounDecodeError, NounEncode}; #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -36,14 +36,15 @@ impl UntypedNoun { .map_err(|e| format!("Failed to cue noun: {:?}", e))?; // Decode the noun into type T - T::from_noun(&noun).map_err(|e| format!("Failed to decode noun: {:?}", e)) + let space = slab.noun_space(); + T::from_noun(&noun, &space).map_err(|e| format!("Failed to decode noun: {:?}", e)) } } impl NounDecode for UntypedNoun { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let mut slab: NounSlab = NounSlab::new(); - slab.copy_into(*noun); + slab.copy_into(*noun, space); let noun_bytes: Bytes = slab.jam(); Ok(UntypedNoun { p: noun_bytes }) } diff --git a/tx-types/src/hashing/hashable.rs b/tx-types/src/hashing/hashable.rs index bc494a3..ca06f77 100644 --- a/tx-types/src/hashing/hashable.rs +++ b/tx-types/src/hashing/hashable.rs @@ -1,8 +1,8 @@ use crate::transaction_types::Hash; use nockapp::noun::slab::NounSlab; use nockapp::utils::make_tas; -use nockapp::{AtomExt, Bytes}; -use nockvm::noun::{Noun, D, T}; +use nockapp::Bytes; +use nockvm::noun::{Noun, NounAllocator, NounSpace, D, T}; use noun_serde::{NounDecode, NounEncode}; /// Hashable enum mirroring Hoon's hashable type /// This is an intermediate representation for hashing complex structures @@ -164,8 +164,9 @@ impl Hashable { let mut cue_slab: NounSlab = NounSlab::new(); let value_noun = cue_slab .cue_into(Bytes::copy_from_slice(bytes)) - .unwrap_or_else(|_| D(0)); - let copied = slab.copy_into(value_noun); + .expect("Hashable::Leaf must contain valid jam bytes"); + let cue_space = cue_slab.noun_space(); + let copied = slab.copy_into(value_noun, &cue_space); T(slab, &[tag, copied]) } Hashable::Hash(hash) => { @@ -190,6 +191,37 @@ impl Hashable { } } + /// Reject leaves whose stored jam bytes cannot be decoded, so hashing can + /// return a real error instead of a fabricated digest. + pub(crate) fn validate_leaves(&self) -> Result<(), crate::hashing::tip5::Tip5Error> { + use crate::hashing::tip5::Tip5Error; + + match self { + Hashable::Leaf(bytes) => { + let mut slab: NounSlab = NounSlab::new(); + slab.cue_into(Bytes::copy_from_slice(bytes)) + .map(|_| ()) + .map_err(|err| { + Tip5Error::SerializationError(format!( + "Hashable::Leaf contains invalid jam bytes: {:?}", + err + )) + }) + } + Hashable::Cell(left, right) => { + left.validate_leaves()?; + right.validate_leaves() + } + Hashable::List(items) => { + for item in items { + item.validate_leaves()?; + } + Ok(()) + } + Hashable::Hash(_) => Ok(()), + } + } + /// Serialize the hashable structure into jammed bytes of the underlying noun. pub fn to_jam(&self) -> Vec { let mut slab: NounSlab = NounSlab::new(); @@ -200,25 +232,26 @@ impl Hashable { /// Reconstruct a `Hashable` structure from a noun that follows the Hoon /// `%hashable` encoding (used by tx-engine and TIP5 hashing). - pub fn from_noun(noun: &Noun) -> Result { - if let Ok(cell) = noun.as_cell() { + pub fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + let handle = noun.in_space(space); + if let Ok(cell) = handle.as_cell() { if let Ok(tag_atom) = cell.head().as_atom() { if tag_atom.eq_bytes(b"leaf") { - let jam = jam_noun(cell.tail()); + let jam = jam_noun(cell.tail().noun(), space); return Ok(Hashable::Leaf(jam)); } else if tag_atom.eq_bytes(b"hash") { - let digest = - Hash::from_noun(&cell.tail()).map_err(|e| format!("hash decode: {e}"))?; + let digest = Hash::from_noun(&cell.tail().noun(), space) + .map_err(|e| format!("hash decode: {e}"))?; return Ok(Hashable::Hash(digest)); } else if tag_atom.eq_bytes(b"list") { - let items = decode_hashable_list(cell.tail())?; + let items = decode_hashable_list(cell.tail().noun(), space)?; return Ok(Hashable::List(items)); } } Ok(Hashable::Cell( - Box::new(Hashable::from_noun(&cell.head())?), - Box::new(Hashable::from_noun(&cell.tail())?), + Box::new(Hashable::from_noun(&cell.head().noun(), space)?), + Box::new(Hashable::from_noun(&cell.tail().noun(), space)?), )) } else { Err("hashable noun must be a cell".into()) @@ -226,18 +259,19 @@ impl Hashable { } } -fn jam_noun(noun: Noun) -> Vec { +fn jam_noun(noun: Noun, space: &NounSpace) -> Vec { let mut slab: NounSlab = NounSlab::new(); - let copied = slab.copy_into(noun); + let copied = slab.copy_into(noun, space); slab.set_root(copied); slab.jam().to_vec() } -fn decode_hashable_list(noun: Noun) -> Result, String> { +fn decode_hashable_list(noun: Noun, space: &NounSpace) -> Result, String> { let mut items = Vec::new(); let mut cursor = noun; loop { - if let Ok(atom) = cursor.as_atom() { + let handle = cursor.in_space(space); + if let Ok(atom) = handle.as_atom() { let end = atom .as_u64() .map_err(|e| format!("list terminator decode: {e}"))?; @@ -247,11 +281,11 @@ fn decode_hashable_list(noun: Noun) -> Result, String> { break; } - let cell = cursor + let cell = handle .as_cell() .map_err(|_| "expected cell while decoding hashable list".to_string())?; - items.push(Hashable::from_noun(&cell.head())?); - cursor = cell.tail(); + items.push(Hashable::from_noun(&cell.head().noun(), space)?); + cursor = cell.tail().noun(); } Ok(items) } diff --git a/tx-types/src/hashing/hasher.rs b/tx-types/src/hashing/hasher.rs index c6074fd..bca075d 100644 --- a/tx-types/src/hashing/hasher.rs +++ b/tx-types/src/hashing/hasher.rs @@ -1,10 +1,10 @@ /// Core hashing functions for the transaction hashing system /// Implements TIP5 hashing compatible with the Hoon implementation use super::hashable::Hashable; +use super::tip5::Tip5Error; use crate::transaction_types::Hash; use nockapp::noun::slab::NounSlab; -use nockapp::Noun; -use nockvm::noun::Atom; +use nockvm::noun::NounAllocator; /// Goldilocks prime: 2^64 - 2^32 + 1 pub const GOLDILOCKS_PRIME: u64 = 0xffffffff00000001; @@ -16,24 +16,20 @@ pub const GOLDILOCKS_PRIME: u64 = 0xffffffff00000001; /// /// This mirrors the Hoon hash-noun-varlen function where the input is /// a noun that gets hashed directly. -pub fn hash_noun_varlen(data: &[u8]) -> Hash { +pub fn hash_noun_varlen(data: &[u8]) -> Result { use super::tip5::Tip5Hasher; use nockapp::Bytes; let mut slab: NounSlab = NounSlab::new(); - // Cue the jammed bytes back to a noun + // Cue the jammed bytes back to a noun. let noun = slab .cue_into(Bytes::copy_from_slice(data)) - .unwrap_or_else(|_e| { - // If cue fails, return atom 0 as fallback - use nockvm::noun::Atom; - Atom::new(&mut slab, 0).as_noun() - }); + .map_err(|e| Tip5Error::SerializationError(format!("failed to cue noun: {:?}", e)))?; // Use the real TIP5 hasher - let result = Tip5Hasher::hash_noun_varlen(noun).unwrap_or_else(|_e| Hash { values: [0; 5] }); - result + let space = slab.noun_space(); + Tip5Hasher::hash_noun_varlen(noun, &space) } /// Hash two digests together (hash-ten-cell in Hoon). @@ -58,13 +54,18 @@ pub fn hash_ten_cell(left: Hash, right: Hash) -> Hash { right.values[4], ]; - Tip5Hasher::hash_10_from_values(&values).unwrap_or_else(|_| Hash { values: [0; 5] }) + // `hash_10_from_values` is pure arithmetic over already-validated limbs + // and cannot fail; keep the infallible signature. + Tip5Hasher::hash_10_from_values(&values).expect("hash_10 over 5+5 u64 limbs is infallible") } /// Recursively hash a Hashable structure -/// This mirrors the Hoon hash-hashable function -pub fn hash_hashable(h: &Hashable) -> Hash { - super::tip5::Tip5Hasher::hash_hashable(h).unwrap_or_else(|_| Hash { values: [0; 5] }) +/// This mirrors the Hoon hash-hashable function. +/// +/// Returns an error instead of a fabricated digest when a leaf cannot be +/// decoded or contains an unbased atom (an atom that does not fit in a belt). +pub fn hash_hashable(h: &Hashable) -> Result { + super::tip5::Tip5Hasher::hash_hashable(h) } /// Convert a digest to a base58 string (for display) @@ -129,7 +130,8 @@ pub fn hash_transcript_list( } // Hash the list using TIP5 - Tip5Hasher::hash_varlen(list) + let space = slab.noun_space(); + Tip5Hasher::hash_varlen(list, &space) } #[cfg(test)] @@ -139,7 +141,7 @@ mod tests { #[test] fn test_hash_hashable_leaf() { let h = Hashable::leaf_from_tas("test"); - let digest = hash_hashable(&h); + let digest = hash_hashable(&h).expect("leaf must hash"); // Just check it doesn't panic and produces a digest assert_eq!(digest.values.len(), 5); } @@ -150,7 +152,7 @@ mod tests { Hashable::leaf_from_tas("left"), Hashable::leaf_from_tas("right"), ); - let digest = hash_hashable(&h); + let digest = hash_hashable(&h).expect("cell must hash"); assert_eq!(digest.values.len(), 5); } @@ -160,7 +162,7 @@ mod tests { values: [1, 2, 3, 4, 5], }; let h = Hashable::Hash(digest.clone()); - let result = hash_hashable(&h); + let result = hash_hashable(&h).expect("precomputed hash must pass through"); assert_eq!(result, digest); } diff --git a/tx-types/src/hashing/raw.rs b/tx-types/src/hashing/raw.rs index 880f5b3..f21c4f4 100644 --- a/tx-types/src/hashing/raw.rs +++ b/tx-types/src/hashing/raw.rs @@ -3,92 +3,107 @@ use crate::hashing::hasher::hash_hashable; use crate::transaction_types::{Hash, SchnorrPubkey, SchnorrSignature}; use crate::transaction_types_v1::LockMerkleProof; use nockapp::noun::slab::NounSlab; -use nockvm::noun::{Atom, Cell, Noun, D}; +use nockvm::noun::{CellHandle, Noun, NounSpace}; use noun_serde::{NounDecode, NounDecodeError}; -fn noun_to_hex(noun: Noun) -> String { +fn tip5_decode_error(err: crate::hashing::tip5::Tip5Error) -> NounDecodeError { + NounDecodeError::Custom(format!("tip5 hashing failed: {}", err)) +} + +fn noun_to_hex(noun: Noun, space: &NounSpace) -> String { let mut slab: NounSlab = NounSlab::new(); - let copy = slab.copy_into(noun); + let copy = slab.copy_into(noun, space); slab.set_root(copy); let jam = slab.jam(); jam.iter().map(|byte| format!("{:02x}", byte)).collect() } -fn expect_cell(noun: Noun, ctx: &str) -> Result { - noun.as_cell().map_err(|_| { +fn expect_cell<'a>( + noun: Noun, + space: &'a NounSpace, + ctx: &str, +) -> Result, NounDecodeError> { + noun.in_space(space).as_cell().map_err(|_| { NounDecodeError::Custom(format!( "{}: expected cell, found {}", ctx, - noun_to_hex(noun) + noun_to_hex(noun, space) )) }) } -fn noun_is_null(noun: Noun) -> bool { - if let Ok(atom) = noun.as_atom() { +fn noun_is_null(noun: Noun, space: &NounSpace) -> bool { + if let Ok(atom) = noun.in_space(space).as_atom() { atom.as_u64().map(|v| v == 0).unwrap_or(false) } else { false } } -fn hashable_leaf_from_noun(noun: Noun) -> Hashable { +fn hashable_leaf_from_noun(noun: Noun, space: &NounSpace) -> Hashable { let mut slab: NounSlab = NounSlab::new(); - let copy = slab.copy_into(noun); + let copy = slab.copy_into(noun, space); slab.set_root(copy); Hashable::Leaf(slab.jam().to_vec()) } -fn hashable_any_noun(noun: Noun) -> Hashable { - if let Ok(cell) = noun.as_cell() { +fn hashable_any_noun(noun: Noun, space: &NounSpace) -> Hashable { + if let Ok(cell) = noun.in_space(space).as_cell() { Hashable::cell( - hashable_any_noun(cell.head()), - hashable_any_noun(cell.tail()), + hashable_any_noun(cell.head().noun(), space), + hashable_any_noun(cell.tail().noun(), space), ) } else { - hashable_leaf_from_noun(noun) + hashable_leaf_from_noun(noun, space) } } -fn extract_spend_payload(spend_noun: Noun) -> Option<(u64, Noun)> { - let cell = spend_noun.as_cell().ok()?; +fn extract_spend_payload(spend_noun: Noun, space: &NounSpace) -> Option<(u64, Noun)> { + let cell = spend_noun.in_space(space).as_cell().ok()?; let head_atom = cell.head().as_atom().ok()?.as_u64().ok()?; if head_atom <= 1 { - Some((head_atom, cell.tail())) + Some((head_atom, cell.tail().noun())) } else { let body = cell.tail(); let body_cell = body.as_cell().ok()?; let variant = body_cell.head().as_atom().ok()?.as_u64().ok()?; - Some((variant, body_cell.tail())) + Some((variant, body_cell.tail().noun())) } } -fn hashable_note_data_from_noun(noun: Noun) -> Result { - if noun_is_null(noun) { - return Ok(hashable_leaf_from_noun(noun)); +fn hashable_note_data_from_noun( + noun: Noun, + space: &NounSpace, +) -> Result { + if noun_is_null(noun, space) { + return Ok(hashable_leaf_from_noun(noun, space)); } - let cell = expect_cell(noun, "note-data node")?; - let node = expect_cell(cell.head(), "note-data key-value pair")?; - let key = hashable_leaf_from_noun(node.head()); - let value = hashable_any_noun(node.tail()); - let children = expect_cell(cell.tail(), "note-data children")?; - let left = hashable_note_data_from_noun(children.head())?; - let right = hashable_note_data_from_noun(children.tail())?; + let cell = expect_cell(noun, space, "note-data node")?; + let node = expect_cell(cell.head().noun(), space, "note-data key-value pair")?; + let key = hashable_leaf_from_noun(node.head().noun(), space); + let value = hashable_any_noun(node.tail().noun(), space); + let children = expect_cell(cell.tail().noun(), space, "note-data children")?; + let left = hashable_note_data_from_noun(children.head().noun(), space)?; + let right = hashable_note_data_from_noun(children.tail().noun(), space)?; Ok(Hashable::triple(Hashable::cell(key, value), left, right)) } -fn hashable_seed_regular_from_noun(noun: Noun) -> Result { - let cell = expect_cell(noun, "seed node")?; - let tail1 = expect_cell(cell.tail(), "seed tail1")?; - let lock_root = Hash::from_noun(&tail1.head())?; - let tail2 = expect_cell(tail1.tail(), "seed tail2")?; - let note_data = tail2.head(); - let tail3 = expect_cell(tail2.tail(), "seed tail3")?; - let gift = tail3.head(); - let parent_hash = Hash::from_noun(&tail3.tail())?; - - let note_data_hash = hash_hashable(&hashable_note_data_from_noun(note_data)?); - let gift_leaf = hashable_leaf_from_noun(gift); +fn hashable_seed_regular_from_noun( + noun: Noun, + space: &NounSpace, +) -> Result { + let cell = expect_cell(noun, space, "seed node")?; + let tail1 = expect_cell(cell.tail().noun(), space, "seed tail1")?; + let lock_root = Hash::from_noun(&tail1.head().noun(), space)?; + let tail2 = expect_cell(tail1.tail().noun(), space, "seed tail2")?; + let note_data = tail2.head().noun(); + let tail3 = expect_cell(tail2.tail().noun(), space, "seed tail3")?; + let gift = tail3.head().noun(); + let parent_hash = Hash::from_noun(&tail3.tail().noun(), space)?; + + let note_data_hash = hash_hashable(&hashable_note_data_from_noun(note_data, space)?) + .map_err(tip5_decode_error)?; + let gift_leaf = hashable_leaf_from_noun(gift, space); Ok(Hashable::cell( Hashable::Hash(lock_root), @@ -99,40 +114,46 @@ fn hashable_seed_regular_from_noun(noun: Noun) -> Result Result { - if noun_is_null(noun) { - return Ok(hashable_leaf_from_noun(noun)); +fn hashable_seeds_regular_from_noun( + noun: Noun, + space: &NounSpace, +) -> Result { + if noun_is_null(noun, space) { + return Ok(hashable_leaf_from_noun(noun, space)); } - let cell = expect_cell(noun, "seeds node")?; - let seed_hashable = hashable_seed_regular_from_noun(cell.head())?; - let children = expect_cell(cell.tail(), "seeds children")?; - let left = hashable_seeds_regular_from_noun(children.head())?; - let right = hashable_seeds_regular_from_noun(children.tail())?; + let cell = expect_cell(noun, space, "seeds node")?; + let seed_hashable = hashable_seed_regular_from_noun(cell.head().noun(), space)?; + let children = expect_cell(cell.tail().noun(), space, "seeds children")?; + let left = hashable_seeds_regular_from_noun(children.head().noun(), space)?; + let right = hashable_seeds_regular_from_noun(children.tail().noun(), space)?; Ok(Hashable::triple(seed_hashable, left, right)) } -fn split_witness_noun(noun: Noun) -> Result<(Noun, Noun, Noun, Noun), NounDecodeError> { - let cell = expect_cell(noun, "witness root")?; - let lmp = cell.head(); - let rest = expect_cell(cell.tail(), "witness tail")?; - let pkh = rest.head(); - let tail = expect_cell(rest.tail(), "witness tail tail")?; - let hax = tail.head(); - let tim = tail.tail(); +fn split_witness_noun( + noun: Noun, + space: &NounSpace, +) -> Result<(Noun, Noun, Noun, Noun), NounDecodeError> { + let cell = expect_cell(noun, space, "witness root")?; + let lmp = cell.head().noun(); + let rest = expect_cell(cell.tail().noun(), space, "witness tail")?; + let pkh = rest.head().noun(); + let tail = expect_cell(rest.tail().noun(), space, "witness tail tail")?; + let hax = tail.head().noun(); + let tim = tail.tail().noun(); Ok((lmp, pkh, hax, tim)) } -fn hashable_hax_from_noun(noun: Noun) -> Result { - if noun_is_null(noun) { - return Ok(hashable_leaf_from_noun(noun)); +fn hashable_hax_from_noun(noun: Noun, space: &NounSpace) -> Result { + if noun_is_null(noun, space) { + return Ok(hashable_leaf_from_noun(noun, space)); } - let cell = expect_cell(noun, "hax node")?; - let pair = expect_cell(cell.head(), "hax pair")?; - let key = Hash::from_noun(&pair.head())?; - let value_hashable = hashable_any_noun(pair.tail()); - let children = expect_cell(cell.tail(), "hax children")?; - let left = hashable_hax_from_noun(children.head())?; - let right = hashable_hax_from_noun(children.tail())?; + let cell = expect_cell(noun, space, "hax node")?; + let pair = expect_cell(cell.head().noun(), space, "hax pair")?; + let key = Hash::from_noun(&pair.head().noun(), space)?; + let value_hashable = hashable_any_noun(pair.tail().noun(), space); + let children = expect_cell(cell.tail().noun(), space, "hax children")?; + let left = hashable_hax_from_noun(children.head().noun(), space)?; + let right = hashable_hax_from_noun(children.tail().noun(), space)?; Ok(Hashable::triple( Hashable::cell(Hashable::Hash(key), value_hashable), left, @@ -140,12 +161,14 @@ fn hashable_hax_from_noun(noun: Noun) -> Result { )) } -fn hashable_witness_from_noun(noun: Noun) -> Result { - let (lmp_noun, pkh_noun, hax_noun, tim_noun) = split_witness_noun(noun)?; - let lmp = LockMerkleProof::from_noun(&lmp_noun)?; - let hax_hash = hash_hashable(&hashable_hax_from_noun(hax_noun)?); - let tim_leaf = hashable_leaf_from_noun(tim_noun); - let pkh_hash = hash_hashable(&hashable_pkh_signature_from_noun(pkh_noun)?); +fn hashable_witness_from_noun(noun: Noun, space: &NounSpace) -> Result { + let (lmp_noun, pkh_noun, hax_noun, tim_noun) = split_witness_noun(noun, space)?; + let lmp = LockMerkleProof::from_noun(&lmp_noun, space)?; + let hax_hash = + hash_hashable(&hashable_hax_from_noun(hax_noun, space)?).map_err(tip5_decode_error)?; + let tim_leaf = hashable_leaf_from_noun(tim_noun, space); + let pkh_hash = hash_hashable(&hashable_pkh_signature_from_noun(pkh_noun, space)?) + .map_err(tip5_decode_error)?; Ok(Hashable::cell_chain([ Hashable::Hash(lmp.to_hash()), @@ -155,35 +178,38 @@ fn hashable_witness_from_noun(noun: Noun) -> Result { ])) } -fn hashable_nname_from_noun(noun: Noun) -> Result { - let cell = expect_cell(noun, "nname pair root")?; - let first = Hash::from_noun(&cell.head())?; - let tail = expect_cell(cell.tail(), "nname tail")?; - let second = Hash::from_noun(&tail.head())?; - let end = tail.tail(); +fn hashable_nname_from_noun(noun: Noun, space: &NounSpace) -> Result { + let cell = expect_cell(noun, space, "nname pair root")?; + let first = Hash::from_noun(&cell.head().noun(), space)?; + let tail = expect_cell(cell.tail().noun(), space, "nname tail")?; + let second = Hash::from_noun(&tail.head().noun(), space)?; + let end = tail.tail().noun(); Ok(Hashable::triple( Hashable::Hash(first), Hashable::Hash(second), - hashable_leaf_from_noun(end), + hashable_leaf_from_noun(end, space), )) } -fn hashable_spend_v1_from_noun(spend_noun: Noun) -> Result { +fn hashable_spend_v1_from_noun( + spend_noun: Noun, + space: &NounSpace, +) -> Result { let (variant, payload) = - extract_spend_payload(spend_noun).ok_or(NounDecodeError::ExpectedCell)?; + extract_spend_payload(spend_noun, space).ok_or(NounDecodeError::ExpectedCell)?; if variant != 1 { return Err(NounDecodeError::Custom("unsupported spend variant".into())); } - let parts = expect_cell(payload, "spend parts")?; - let witness_noun = parts.head(); - let tail = expect_cell(parts.tail(), "spend tail (seeds+fee)")?; - let seeds_noun = tail.head(); - let fee_noun = tail.tail(); + let parts = expect_cell(payload, space, "spend parts")?; + let witness_noun = parts.head().noun(); + let tail = expect_cell(parts.tail().noun(), space, "spend tail (seeds+fee)")?; + let seeds_noun = tail.head().noun(); + let fee_noun = tail.tail().noun(); - let witness_hashable = hashable_witness_from_noun(witness_noun)?; - let seeds_hashable = hashable_seeds_regular_from_noun(seeds_noun)?; - let fee_leaf = hashable_leaf_from_noun(fee_noun); + let witness_hashable = hashable_witness_from_noun(witness_noun, space)?; + let seeds_hashable = hashable_seeds_regular_from_noun(seeds_noun, space)?; + let fee_leaf = hashable_leaf_from_noun(fee_noun, space); Ok(Hashable::cell( Hashable::leaf_from_atom(1u64.to_le_bytes()), @@ -191,104 +217,131 @@ fn hashable_spend_v1_from_noun(spend_noun: Noun) -> Result Result<(Noun, Noun), NounDecodeError> { - if noun_is_null(children) { +fn split_children(children: Noun, space: &NounSpace) -> Result<(Noun, Noun), NounDecodeError> { + if noun_is_null(children, space) { return Ok((children, children)); } - let child_cell = expect_cell(children, "spends children pair")?; - Ok((child_cell.head(), child_cell.tail())) + let child_cell = expect_cell(children, space, "spends children pair")?; + Ok((child_cell.head().noun(), child_cell.tail().noun())) } -fn hashable_spends_from_noun(spends: Noun) -> Result { - if noun_is_null(spends) { - return Ok(hashable_leaf_from_noun(spends)); +fn hashable_spends_from_noun(spends: Noun, space: &NounSpace) -> Result { + if noun_is_null(spends, space) { + return Ok(hashable_leaf_from_noun(spends, space)); } - let cell = expect_cell(spends, "spends node")?; - let node = cell.head(); - let (left, right) = split_children(cell.tail())?; + let cell = expect_cell(spends, space, "spends node")?; + let node = cell.head().noun(); + let (left, right) = split_children(cell.tail().noun(), space)?; - let pair = expect_cell(node, "spends node key/value pair")?; - let name_noun = pair.head(); - let spend_noun = pair.tail(); + let pair = expect_cell(node, space, "spends node key/value pair")?; + let name_noun = pair.head().noun(); + let spend_noun = pair.tail().noun(); let node_hashable = Hashable::cell( - hashable_nname_from_noun(name_noun)?, - hashable_spend_v1_from_noun(spend_noun)?, + hashable_nname_from_noun(name_noun, space)?, + hashable_spend_v1_from_noun(spend_noun, space)?, ); Ok(Hashable::triple( node_hashable, - hashable_spends_from_noun(left)?, - hashable_spends_from_noun(right)?, + hashable_spends_from_noun(left, space)?, + hashable_spends_from_noun(right, space)?, )) } -fn hashable_pkh_signature_from_noun(noun: Noun) -> Result { - if noun_is_null(noun) { - return Ok(hashable_leaf_from_noun(noun)); +fn hashable_pkh_signature_from_noun( + noun: Noun, + space: &NounSpace, +) -> Result { + if noun_is_null(noun, space) { + return Ok(hashable_leaf_from_noun(noun, space)); } - let cell = expect_cell(noun, "pkh-signature node")?; - let node = cell.head(); - let (left, right) = split_children(cell.tail())?; + let cell = expect_cell(noun, space, "pkh-signature node")?; + let node = cell.head().noun(); + let (left, right) = split_children(cell.tail().noun(), space)?; - let pair = expect_cell(node, "pkh-signature kv pair")?; - let key_hash = Hash::from_noun(&pair.head())?; - let value_hashable = hashable_pkh_signature_value_from_noun(pair.tail())?; + let pair = expect_cell(node, space, "pkh-signature kv pair")?; + let key_hash = Hash::from_noun(&pair.head().noun(), space)?; + let value_hashable = hashable_pkh_signature_value_from_noun(pair.tail().noun(), space)?; let node_hashable = Hashable::cell(Hashable::Hash(key_hash), value_hashable); Ok(Hashable::triple( node_hashable, - hashable_pkh_signature_from_noun(left)?, - hashable_pkh_signature_from_noun(right)?, + hashable_pkh_signature_from_noun(left, space)?, + hashable_pkh_signature_from_noun(right, space)?, )) } -fn hashable_pkh_signature_value_from_noun(noun: Noun) -> Result { - let value_cell = expect_cell(noun, "pkh-signature value")?; - let pk_noun = value_cell.head(); - let sig_noun = value_cell.tail(); - let pk = SchnorrPubkey::from_noun(&pk_noun)?; - let sig = SchnorrSignature::from_noun(&sig_noun)?; +fn hashable_pkh_signature_value_from_noun( + noun: Noun, + space: &NounSpace, +) -> Result { + let value_cell = expect_cell(noun, space, "pkh-signature value")?; + let pk_noun = value_cell.head().noun(); + let sig_noun = value_cell.tail().noun(); + let pk = SchnorrPubkey::from_noun(&pk_noun, space)?; + let sig = SchnorrSignature::from_noun(&sig_noun, space)?; Ok(Hashable::cell( Hashable::Hash(pk.to_hash()), sig.to_hashable(), )) } -pub fn hashable_spend_from_raw(spend_noun: Noun) -> Result { - hashable_spend_v1_from_noun(spend_noun) +pub fn hashable_spend_from_raw( + spend_noun: Noun, + space: &NounSpace, +) -> Result { + hashable_spend_v1_from_noun(spend_noun, space) } -pub fn hashable_witness_from_raw(witness_noun: Noun) -> Result { - hashable_witness_from_noun(witness_noun) +pub fn hashable_witness_from_raw( + witness_noun: Noun, + space: &NounSpace, +) -> Result { + hashable_witness_from_noun(witness_noun, space) } -pub fn hashable_seeds_from_raw(seeds_noun: Noun) -> Result { - hashable_seeds_regular_from_noun(seeds_noun) +pub fn hashable_seeds_from_raw( + seeds_noun: Noun, + space: &NounSpace, +) -> Result { + hashable_seeds_regular_from_noun(seeds_noun, space) } -pub fn hashable_note_data_from_raw(noun: Noun) -> Result { - hashable_note_data_from_noun(noun) +pub fn hashable_note_data_from_raw( + noun: Noun, + space: &NounSpace, +) -> Result { + hashable_note_data_from_noun(noun, space) } -pub fn hashable_pkh_signature_from_raw(noun: Noun) -> Result { - hashable_pkh_signature_from_noun(noun) +pub fn hashable_pkh_signature_from_raw( + noun: Noun, + space: &NounSpace, +) -> Result { + hashable_pkh_signature_from_noun(noun, space) } -pub fn hashable_pkh_signature_direct(noun: Noun) -> Hashable { - hashable_leaf_from_noun(noun) +pub fn hashable_pkh_signature_direct(noun: Noun, space: &NounSpace) -> Hashable { + hashable_leaf_from_noun(noun, space) } -pub fn hashable_spends_from_raw(spends_noun: Noun) -> Result { - hashable_spends_from_noun(spends_noun) +pub fn hashable_spends_from_raw( + spends_noun: Noun, + space: &NounSpace, +) -> Result { + hashable_spends_from_noun(spends_noun, space) } -pub fn compute_tx_id_v1_from_raw_noun(spends_noun: Noun) -> Result { - let spends_hashable = hashable_spends_from_noun(spends_noun)?; +pub fn compute_tx_id_v1_from_raw_noun( + spends_noun: Noun, + space: &NounSpace, +) -> Result { + let spends_hashable = hashable_spends_from_noun(spends_noun, space)?; let version_leaf = Hashable::leaf_from_atom(1u64.to_le_bytes()); let hashable = Hashable::cell(version_leaf, spends_hashable); - Ok(hash_hashable(&hashable)) + hash_hashable(&hashable).map_err(tip5_decode_error) } diff --git a/tx-types/src/hashing/tip5.rs b/tx-types/src/hashing/tip5.rs index f4b4025..7a2535b 100644 --- a/tx-types/src/hashing/tip5.rs +++ b/tx-types/src/hashing/tip5.rs @@ -1,15 +1,14 @@ use nockapp::noun::slab::NounSlab; use nockapp::Noun; -use nockchain_libp2p_io::tip5_util; - // Re-export for use by other modules in this crate +use nockapp::noun::NounAllocatorExt; pub use nockchain_libp2p_io::tip5_util::tip5_hash_to_base58; +use nockchain_math::shape::do_leaf_sequence; +use nockchain_math::tip5::hash::{hash_belts_slice, hash_noun_varlen_digest, hash_ten_cell}; use nockvm::jets::util::test::init_context; use nockvm::jets::JetErr; -use nockvm::noun::{D, T}; -use zkvm_jetpack::jets::tip5_jets::{ - hash_10_jet, hash_hashable_jet, hash_noun_varlen_jet, hash_varlen_jet, -}; +use nockvm::noun::{NounAllocator, NounSpace}; +use zkvm_jetpack::jets::tip5_jets::hash_hashable as hash_hashable_noun; use crate::transaction_types::Hash; @@ -49,13 +48,14 @@ impl Tip5Hasher { /// Parse a hash noun which can be either: /// - A 5-tuple: [a [b [c [d e]]]] (from digest_to_noundigest) /// - A Hoon list with terminator: [a [b [c [d [e 0]]]]] (from vec_to_hoon_list) - fn digest_from_noun(hash_noun: Noun) -> Result { + fn digest_from_noun(hash_noun: Noun, space: &NounSpace) -> Result { let mut values = [0u64; 5]; let mut current = hash_noun; // Parse first 4 elements (always in cells) for i in 0..4 { let cell = current + .in_space(space) .as_cell() .map_err(|_| Tip5Error::HashingError(format!("Expected cell at position {}", i)))?; @@ -68,26 +68,27 @@ impl Tip5Hasher { Tip5Error::HashingError(format!("Atom too large at position {}", i)) })?; - current = cell.tail(); + current = cell.tail().noun(); } // The 5th element: could be either: // - An atom (5-tuple format: [a [b [c [d e]]]]) // - A cell [e 0] (Hoon list format: [a [b [c [d [e 0]]]]]) - if let Ok(atom) = current.as_atom() { + let current_handle = current.in_space(space); + if let Ok(atom) = current_handle.as_atom() { // 5-tuple format - current is already the 5th element - values[4] = atom.as_u64().map_err(|_| { - Tip5Error::HashingError("Atom too large at position 4".to_string()) - })?; - } else if let Ok(cell) = current.as_cell() { + values[4] = atom + .as_u64() + .map_err(|_| Tip5Error::HashingError("Atom too large at position 4".to_string()))?; + } else if let Ok(cell) = current_handle.as_cell() { // Hoon list format - extract head of [e 0] let atom = cell .head() .as_atom() .map_err(|_| Tip5Error::HashingError("Expected atom at position 4".to_string()))?; - values[4] = atom.as_u64().map_err(|_| { - Tip5Error::HashingError("Atom too large at position 4".to_string()) - })?; + values[4] = atom + .as_u64() + .map_err(|_| Tip5Error::HashingError("Atom too large at position 4".to_string()))?; } else { return Err(Tip5Error::HashingError( "Invalid hash noun structure at position 4".to_string(), @@ -104,21 +105,17 @@ impl Tip5Hasher { /// /// # Arguments /// * `noun` - The noun to hash + /// * `space` - The [`NounSpace`] `noun` was allocated in /// /// # Returns /// * `Result` - The hash as a Hash struct or an error - pub fn hash_noun_varlen(noun: Noun) -> Result { - // Create a test context (includes NockStack and other needed components) + pub fn hash_noun_varlen(noun: Noun, space: &NounSpace) -> Result { + // The digest walk reads the noun directly in its own space and does not + // allocate, so no scratch stack copy is needed. let context = &mut init_context(); - - // Create the proper subject structure for jets: [formula sample payload] - // The jet expects the sample at slot 6, so we need [0 sample 0] - let subject = T(&mut context.stack, &[D(0), noun, D(0)]); - - let hash_noun = hash_noun_varlen_jet(context, subject) + let values = hash_noun_varlen_digest(&mut context.stack, noun, space) .map_err(|e| Tip5Error::HashingError(format!("TIP5 hashing failed: {:?}", e)))?; - - Self::digest_from_noun(hash_noun) + Ok(Hash { values }) } /// Hash a 10-tuple using the TIP5 hash_10 algorithm @@ -128,21 +125,20 @@ impl Tip5Hasher { /// /// # Arguments /// * `ten_list` - A noun that should be a 10-element list + /// * `space` - The [`NounSpace`] `ten_list` was allocated in /// /// # Returns /// * `Result` - The hash as a Hash struct or an error - pub fn hash_10(ten_list: Noun) -> Result { - // Create a test context (includes NockStack and other needed components) - let context = &mut init_context(); - - // Create the proper subject structure for jets: [formula sample payload] - // The jet expects the sample at slot 6, so we need [0 sample 0] - let subject = T(&mut context.stack, &[D(0), ten_list, D(0)]); - - let hash_noun = hash_10_jet(context, subject) + pub fn hash_10(ten_list: Noun, space: &NounSpace) -> Result { + let mut leaf = Vec::::new(); + do_leaf_sequence(ten_list, &mut leaf, space) .map_err(|e| Tip5Error::HashingError(format!("TIP5 hash_10 failed: {:?}", e)))?; - - Self::digest_from_noun(hash_noun) + let leaf: [u64; nockchain_math::tip5::RATE] = leaf.try_into().map_err(|_| { + Tip5Error::HashingError("hash_10 expects exactly 10 leaves".to_string()) + })?; + Ok(Hash { + values: hash_ten_cell(leaf), + }) } /// Hash 10 u64 values using the TIP5 hash_10 algorithm @@ -155,42 +151,27 @@ impl Tip5Hasher { /// # Returns /// * `Result` - The hash as a Hash struct or an error pub fn hash_10_from_values(values: &[u64; 10]) -> Result { - use nockvm::noun::Atom; - - // Create a test context (includes NockStack and other needed components) - let context = &mut init_context(); - let stack = &mut context.stack; - - // Build a proper Hoon list directly in the NockStack - // [v0 [v1 [v2 [v3 [v4 [v5 [v6 [v7 [v8 [v9 0]]]]]]]]]] - let mut list = D(0); - for &val in values.iter().rev() { - // Use Atom::new to handle large values that exceed DIRECT_MAX - let atom = Atom::new(stack, val).as_noun(); - list = T(stack, &[atom, list]); - } - - // Create the proper subject structure for jets: [formula sample payload] - let subject = T(stack, &[D(0), list, D(0)]); - - let hash_noun = hash_10_jet(context, subject) - .map_err(|e| Tip5Error::HashingError(format!("TIP5 hash_10 failed: {:?}", e)))?; - - Self::digest_from_noun(hash_noun) + Ok(Hash { + values: hash_ten_cell(*values), + }) } /// Hash a Hashable structure using the TIP5 jet. pub fn hash_hashable(hashable: &crate::hashing::hashable::Hashable) -> Result { + hashable.validate_leaves()?; let mut slab: NounSlab = NounSlab::new(); let noun = hashable.to_noun(&mut slab); slab.set_root(noun); + let slab_space = slab.noun_space(); let context = &mut init_context(); - let subject = T(&mut context.stack, &[D(0), noun, D(0)]); - let hash_noun = hash_hashable_jet(context, subject) + let copied = context.stack.copy_into(noun, &slab_space); + let stack_space = context.stack.noun_space(); + let hash_noun = hash_hashable_noun(&mut context.stack, copied, &stack_space) .map_err(|e| Tip5Error::HashingError(format!("TIP5 hash_hashable failed: {:?}", e)))?; - Self::digest_from_noun(hash_noun) + let result_space = context.stack.noun_space(); + Self::digest_from_noun(hash_noun, &result_space) } /// Hash a noun and convert to base58 string @@ -199,24 +180,13 @@ impl Tip5Hasher { /// /// # Arguments /// * `noun` - The noun to hash + /// * `space` - The [`NounSpace`] `noun` was allocated in /// /// # Returns /// * `Result` - The base58-encoded hash or an error - pub fn hash_noun_varlen_to_base58(noun: Noun) -> Result { - // Create a test context (includes NockStack and other needed components) - let context = &mut init_context(); - - // Create the proper subject structure for jets: [formula sample payload] - // The jet expects the sample at slot 6, so we need [0 sample 0] - let subject = T(&mut context.stack, &[D(0), noun, D(0)]); - - // Call the TIP5 hash jet - let hash_noun = hash_noun_varlen_jet(context, subject) - .map_err(|e| Tip5Error::HashingError(format!("TIP5 hashing failed: {:?}", e)))?; - - // Convert the 5-tuple hash to base58 string - tip5_util::tip5_hash_to_base58(hash_noun) - .map_err(|e| Tip5Error::HashingError(format!("Base58 conversion failed: {:?}", e))) + pub fn hash_noun_varlen_to_base58(noun: Noun, space: &NounSpace) -> Result { + let hash = Self::hash_noun_varlen(noun, space)?; + Ok(hash.to_b58()) } /// Hash a list of field elements using the TIP5 algorithm @@ -229,67 +199,40 @@ impl Tip5Hasher { /// /// # Returns /// * `Result` - The hash as a Hash struct or an error - pub fn hash_varlen(belts_list: Noun) -> Result { - // Create a test context (includes NockStack and other needed components) - let context = &mut init_context(); - - // Create the proper subject structure for jets: [formula sample payload] - // The jet expects the sample at slot 6, so we need [0 sample 0] - let subject = T(&mut context.stack, &[D(0), belts_list, D(0)]); - - // Call the TIP5 hash_varlen jet - returns a list (not a tuple) - let hash_noun = hash_varlen_jet(context, subject) - .map_err(|e| Tip5Error::HashingError(format!("TIP5 hash_varlen failed: {:?}", e)))?; - - // Extract the 5 u64 values from the list - // The result is a list structure: [a [b [c [d [e 0]]]]]] - let mut values = [0u64; 5]; - let mut current = hash_noun; - - for i in 0..5 { - let cell = current - .as_cell() - .map_err(|_| Tip5Error::HashingError(format!("Expected cell at position {}", i)))?; - - // Get the atom - let atom = cell - .head() + pub fn hash_varlen(belts_list: Noun, space: &NounSpace) -> Result { + let mut values = Vec::new(); + let mut current = belts_list; + + loop { + let handle = current.in_space(space); + if let Ok(cell) = handle.as_cell() { + let atom = cell + .head() + .as_atom() + .map_err(|_| Tip5Error::HashingError("Expected atom in list".to_string()))?; + values.push( + atom.as_u64().map_err(|_| { + Tip5Error::HashingError("Atom too large in list".to_string()) + })?, + ); + current = cell.tail().noun(); + continue; + } + + let atom = handle .as_atom() - .map_err(|_| Tip5Error::HashingError(format!("Expected atom at position {}", i)))?; - - // Handle the conversion properly - values[i] = if atom.is_direct() { - atom.as_u64().map_err(|_| { - Tip5Error::HashingError(format!("Atom too large at position {}", i)) - })? - } else { - // For indirect atoms, we need to handle the bytes properly - let bytes = atom.as_ne_bytes(); - if bytes.len() > 8 { - return Err(Tip5Error::HashingError(format!( - "Atom too large at position {}", - i - ))); - } - // Reconstruct u64 from little-endian bytes - let mut result = 0u64; - for (j, &byte) in bytes.iter().enumerate() { - result |= (byte as u64) << (j * 8); - } - result - }; - - current = cell.tail(); - } - - // current should now be 0 (end of list) - if current.as_atom().is_ok() && current.as_atom().unwrap().as_u64().unwrap_or(1) != 0 { + .map_err(|_| Tip5Error::HashingError("Expected atom or cell".to_string()))?; + if atom.as_u64().unwrap_or(1) == 0 { + break; + } return Err(Tip5Error::HashingError( "Expected list to end with 0".to_string(), )); } - Ok(Hash { values }) + Ok(Hash { + values: hash_belts_slice(&values), + }) } } @@ -298,12 +241,13 @@ impl Tip5Hasher { #[cfg(test)] mod tests { use super::*; - use nockvm::noun::D; + use nockvm::noun::{D, T}; #[test] fn test_hash_simple_atom() { let noun = D(42); - let hash = Tip5Hasher::hash_noun_varlen(noun).expect("Should hash successfully"); + let space = NounSpace::empty(); + let hash = Tip5Hasher::hash_noun_varlen(noun, &space).expect("Should hash successfully"); // Hash should have 5 u64 values assert_eq!(hash.values.len(), 5); @@ -316,7 +260,7 @@ mod tests { // Test the base58 conversion function let hash_str = - Tip5Hasher::hash_noun_varlen_to_base58(noun).expect("Should convert to base58"); + Tip5Hasher::hash_noun_varlen_to_base58(noun, &space).expect("Should convert to base58"); assert!(!hash_str.is_empty()); assert!(hash_str .chars() @@ -326,8 +270,9 @@ mod tests { #[test] fn test_hash_consistency() { let noun = D(123); - let hash1 = Tip5Hasher::hash_noun_varlen(noun).expect("Should hash successfully"); - let hash2 = Tip5Hasher::hash_noun_varlen(noun).expect("Should hash successfully"); + let space = NounSpace::empty(); + let hash1 = Tip5Hasher::hash_noun_varlen(noun, &space).expect("Should hash successfully"); + let hash2 = Tip5Hasher::hash_noun_varlen(noun, &space).expect("Should hash successfully"); // Same input should produce same hash assert_eq!(hash1.values, hash2.values); @@ -335,8 +280,9 @@ mod tests { #[test] fn test_hash_different_values() { - let hash1 = Tip5Hasher::hash_noun_varlen(D(42)).expect("Should hash successfully"); - let hash2 = Tip5Hasher::hash_noun_varlen(D(43)).expect("Should hash successfully"); + let space = NounSpace::empty(); + let hash1 = Tip5Hasher::hash_noun_varlen(D(42), &space).expect("Should hash successfully"); + let hash2 = Tip5Hasher::hash_noun_varlen(D(43), &space).expect("Should hash successfully"); // Different inputs should produce different hashes assert_ne!(hash1.values, hash2.values); @@ -351,7 +297,9 @@ mod tests { // In Hoon list format: [1 [2 [3 0]]] let list_noun = T(&mut context.stack, &[D(1), D(2), D(3), D(0)]); - let hash = Tip5Hasher::hash_varlen(list_noun).expect("Should hash successfully"); + let stack_space = context.stack.noun_space(); + let hash = + Tip5Hasher::hash_varlen(list_noun, &stack_space).expect("Should hash successfully"); // Hash should have 5 u64 values assert_eq!(hash.values.len(), 5); @@ -363,7 +311,8 @@ mod tests { ); // Test consistency - same input should produce same hash - let hash2 = Tip5Hasher::hash_varlen(list_noun).expect("Should hash successfully"); + let hash2 = + Tip5Hasher::hash_varlen(list_noun, &stack_space).expect("Should hash successfully"); assert_eq!(hash.values, hash2.values, "Hash should be deterministic"); println!("hash_varlen([1, 2, 3]) = {:x?}", hash.values); diff --git a/tx-types/src/hashing/tip5_impl.rs b/tx-types/src/hashing/tip5_impl.rs deleted file mode 100644 index f8c84a4..0000000 --- a/tx-types/src/hashing/tip5_impl.rs +++ /dev/null @@ -1,185 +0,0 @@ -// Minimal TIP5 implementation for tx-types -// This replaces the dependency on zkvm-jetpack's jets - -use nockvm::mem::NockStack; -use nockvm::noun::{Noun, Atom, Cell, D, T}; - -const STATE_SIZE: usize = 16; -const RATE: usize = 8; -const DIGEST_LENGTH: usize = 5; -const P: u64 = 0xffffffff00000001; // Goldilocks prime - -// TIP5 permutation constants -const RC16: [[u64; 16]; 16] = [ - [0, 7, 23, 8, 56, 102, 67, 173, 129, 174, 176, 13, 20, 137, 245, 230], - [0, 86, 221, 4, 82, 244, 223, 128, 44, 92, 35, 25, 33, 46, 58, 254], - [0, 161, 28, 131, 190, 75, 196, 127, 54, 25, 239, 140, 135, 201, 160, 150], - [0, 207, 230, 102, 11, 203, 42, 110, 111, 33, 87, 104, 132, 116, 85, 37], - [0, 98, 161, 113, 76, 19, 103, 39, 74, 129, 243, 254, 148, 46, 16, 144], - [0, 96, 0, 113, 224, 51, 168, 48, 123, 248, 204, 44, 128, 135, 232, 43], - [0, 24, 218, 246, 158, 249, 14, 101, 63, 60, 130, 76, 239, 165, 148, 160], - [0, 243, 134, 50, 237, 197, 173, 181, 146, 148, 238, 191, 44, 15, 195, 141], - [0, 248, 169, 42, 88, 108, 150, 178, 54, 120, 14, 98, 234, 63, 35, 166], - [0, 3, 206, 34, 248, 39, 128, 244, 199, 95, 104, 10, 108, 8, 126, 207], - [0, 165, 13, 148, 162, 162, 209, 227, 112, 25, 218, 51, 174, 224, 89, 218], - [0, 134, 38, 56, 213, 61, 90, 234, 78, 242, 43, 138, 65, 165, 176, 24], - [0, 139, 134, 98, 131, 169, 72, 153, 72, 175, 9, 62, 56, 123, 2, 152], - [0, 148, 10, 178, 129, 91, 229, 219, 220, 32, 87, 160, 184, 79, 225, 155], - [0, 142, 41, 245, 230, 197, 32, 107, 157, 90, 127, 136, 99, 242, 164, 105], - [0, 197, 165, 248, 147, 133, 165, 88, 87, 102, 46, 162, 140, 89, 156, 57], -]; - -// Minimal implementation of TIP5 hashing -pub fn hash_noun_varlen_minimal(stack: &mut NockStack, noun: Noun) -> Result { - // Extract the list of values from the noun - let mut values = Vec::new(); - - if let Ok(cell) = noun.as_cell() { - // It's a list - extract all values - let mut current = noun; - while let Ok(cell) = current.as_cell() { - if let Ok(atom) = cell.head().as_atom() { - if let Ok(val) = atom.as_u64() { - values.push(val); - } - } - current = cell.tail(); - } - } else if let Ok(atom) = noun.as_atom() { - // Single atom - if atom.as_u64().unwrap_or(0) != 0 { - values.push(atom.as_u64().unwrap_or(0)); - } - } - - // Perform simplified TIP5 hash - let digest = hash_values(&values); - - // Convert digest to noun - let n0 = D(digest[0]); - let n1 = D(digest[1]); - let n2 = D(digest[2]); - let n3 = D(digest[3]); - let n4 = D(digest[4]); - - Ok(T(stack, &[n0, n1, n2, n3, n4])) -} - -pub fn hash_10_minimal(stack: &mut NockStack, noun: Noun) -> Result { - // Extract exactly 10 values - let mut values = Vec::new(); - let mut current = noun; - - for _ in 0..10 { - if let Ok(cell) = current.as_cell() { - if let Ok(atom) = cell.head().as_atom() { - values.push(atom.as_u64().unwrap_or(0)); - } - current = cell.tail(); - } - } - - if values.len() != 10 { - return Err("hash_10 requires exactly 10 elements".to_string()); - } - - // Perform hash with fixed initialization - let digest = hash_10_values(&values); - - // Convert to list - let mut result = D(0); - for &val in digest.iter().rev() { - result = T(stack, &[D(val), result]); - } - - Ok(result) -} - -fn hash_values(values: &[u64]) -> [u64; 5] { - let mut state = [0u64; STATE_SIZE]; - - // Pad input - let mut padded = values.to_vec(); - padded.push(1); - while padded.len() % RATE != 0 { - padded.push(0); - } - - // Absorb phase - for chunk in padded.chunks(RATE) { - for (i, &val) in chunk.iter().enumerate() { - state[i] = add_mod(state[i], val); - } - permute(&mut state); - } - - // Extract digest - let mut digest = [0u64; DIGEST_LENGTH]; - for i in 0..DIGEST_LENGTH { - digest[i] = state[i]; - } - - digest -} - -fn hash_10_values(values: &[u64]) -> [u64; 5] { - let mut state = [0u64; STATE_SIZE]; - - // Initialize with fixed values for hash-10 - for i in 10..16 { - state[i] = 0xFFFFFFFF; - } - - // Copy input - for (i, &val) in values.iter().take(10).enumerate() { - state[i] = val; - } - - // Single permutation - permute(&mut state); - - // Extract digest - let mut digest = [0u64; DIGEST_LENGTH]; - for i in 0..DIGEST_LENGTH { - digest[i] = state[i]; - } - - digest -} - -fn permute(state: &mut [u64; STATE_SIZE]) { - // Simplified TIP5 permutation - for round in 0..16 { - // Add round constants - for i in 0..16 { - state[i] = add_mod(state[i], RC16[round][i]); - } - - // S-box (simplified) - for i in 0..16 { - let x = state[i]; - state[i] = mul_mod(x, x); // x^2 mod p - } - - // Linear layer (simplified MDS multiplication) - let mut new_state = [0u64; STATE_SIZE]; - for i in 0..16 { - let mut sum = 0u64; - for j in 0..16 { - sum = add_mod(sum, mul_mod(state[j], ((i + j) % 16 + 1) as u64)); - } - new_state[i] = sum; - } - *state = new_state; - } -} - -fn add_mod(a: u64, b: u64) -> u64 { - let sum = a as u128 + b as u128; - (sum % P as u128) as u64 -} - -fn mul_mod(a: u64, b: u64) -> u64 { - let prod = a as u128 * b as u128; - (prod % P as u128) as u64 -} \ No newline at end of file diff --git a/tx-types/src/hashing/tx_id.rs b/tx-types/src/hashing/tx_id.rs index e320ebd..c99db5a 100644 --- a/tx-types/src/hashing/tx_id.rs +++ b/tx-types/src/hashing/tx_id.rs @@ -30,7 +30,7 @@ pub fn compute_tx_id(inputs: &Inputs, timelock_range: &TimelockRange, total_fees ); // Hash the structure - hash_hashable(&hashable) + hash_hashable(&hashable).expect("v0 tx-id hashable is u64-based") } /// Compute tx-id and return as base58 string @@ -240,7 +240,7 @@ mod tests { ], }; - let computed_hash = hash_hashable(&hashable); + let computed_hash = hash_hashable(&hashable).expect("v0 tx-id hashable is u64-based"); println!("\nNNote hash:"); println!("Expected hash from Hoon: {:x?}", expected_hash.values); @@ -593,7 +593,7 @@ mod tests { } // Now verify the final hash - let computed_hash = hash_hashable(&hashable); + let computed_hash = hash_hashable(&hashable).expect("v0 tx-id hashable is u64-based"); let expected_hash = Hash { values: [ 0x7546_e958_54fa_40c0, @@ -633,7 +633,8 @@ mod tests { ], }; let hash_from_method = nname.to_hash(); - let hash_from_manual = hash_hashable(&nname.to_hashable()); + let hash_from_manual = + hash_hashable(&nname.to_hashable()).expect("nname hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "NName to_hash should match manual approach" @@ -645,7 +646,8 @@ mod tests { max: Some(PageNumber { value: 200 }), }; let hash_from_method = timelock_range.to_hash(); - let hash_from_manual = hash_hashable(&timelock_range.to_hashable()); + let hash_from_manual = hash_hashable(&timelock_range.to_hashable()) + .expect("timelock-range hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "TimelockRange to_hash should match manual approach" @@ -665,7 +667,8 @@ mod tests { )), }; let hash_from_method = timelock.to_hash(); - let hash_from_manual = hash_hashable(&timelock.to_hashable()); + let hash_from_manual = + hash_hashable(&timelock.to_hashable()).expect("timelock hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Timelock to_hash should match manual approach" @@ -679,7 +682,8 @@ mod tests { is_coinbase: false, }; let hash_from_method = source.to_hash(); - let hash_from_manual = hash_hashable(&source.to_hashable()); + let hash_from_manual = + hash_hashable(&source.to_hashable()).expect("source hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Source to_hash should match manual approach" @@ -699,7 +703,8 @@ mod tests { pubkeys.put(pubkey); let lock = Lock { m: 1, pubkeys }; let hash_from_method = lock.to_hash(); - let hash_from_manual = hash_hashable(&lock.to_hashable()); + let hash_from_manual = + hash_hashable(&lock.to_hashable()).expect("lock hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Lock to_hash should match manual approach" @@ -719,7 +724,8 @@ mod tests { }, }; let hash_from_method = seed.to_hash(); - let hash_from_manual = hash_hashable(&seed.to_hashable()); + let hash_from_manual = + hash_hashable(&seed.to_hashable()).expect("seed hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Seed to_hash should match manual approach" @@ -730,7 +736,8 @@ mod tests { seed_set.put(seed); let seeds = Seeds { set: seed_set }; let hash_from_method = seeds.to_hash(); - let hash_from_manual = hash_hashable(&seeds.to_hashable()); + let hash_from_manual = + hash_hashable(&seeds.to_hashable()).expect("seeds hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Seeds to_hash should match manual approach" @@ -743,7 +750,8 @@ mod tests { fee: Coins { value: 10 }, }; let hash_from_method = spend.to_hash(); - let hash_from_manual = hash_hashable(&spend.to_hashable()); + let hash_from_manual = + hash_hashable(&spend.to_hashable()).expect("spend hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Spend to_hash should match manual approach" @@ -775,7 +783,8 @@ mod tests { spend: spend.clone(), }; let hash_from_method = input.to_hash(); - let hash_from_manual = hash_hashable(&input.to_hashable()); + let hash_from_manual = + hash_hashable(&input.to_hashable()).expect("input hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "Input to_hash should match manual approach" @@ -783,7 +792,8 @@ mod tests { // Test NNote's to_hash (which already existed) let hash_from_method = nnote.to_hash(); - let hash_from_manual = hash_hashable(&nnote.to_hashable()); + let hash_from_manual = + hash_hashable(&nnote.to_hashable()).expect("note hashable is u64-based"); assert_eq!( hash_from_method, hash_from_manual, "NNote to_hash should match manual approach" diff --git a/tx-types/src/hashing/u320.rs b/tx-types/src/hashing/u320.rs index 900072e..f441e7d 100644 --- a/tx-types/src/hashing/u320.rs +++ b/tx-types/src/hashing/u320.rs @@ -47,6 +47,7 @@ impl U320 { mod tests { use super::U320; use crate::transaction_types::Hash; + use num_bigint::BigUint; #[test] fn test_u320_from_b58_basic() { @@ -91,4 +92,41 @@ mod tests { assert!(result.is_err()); assert!(result.unwrap_err().contains("too large")); } + + #[test] + fn test_from_b58_rejects_out_of_domain_high_quotient() { + // p^5 needs a sixth base-p limb, so it is outside the five-limb tip5 + // domain. It used to be truncated to the all-zero digest. + let p5 = BigUint::from(0xFFFF_FFFF_0000_0001u64).pow(5); + let s = bs58::encode(p5.to_bytes_be()).into_string(); + let err = Hash::from_b58(&s).expect_err("p^5 must be rejected"); + assert!(err.contains("too large"), "unexpected error: {err}"); + } + + #[test] + fn test_from_b58_rejects_non_canonical_leading_one() { + let canonical = Hash { + values: [1, 2, 3, 4, 1000], + } + .to_b58(); + Hash::from_b58(&canonical).expect("canonical hash decodes"); + + // A leading '1' is a leading zero byte: same limbs, non-canonical + // spelling, and must not alias the canonical hash. + let padded = format!("1{canonical}"); + let err = Hash::from_b58(&padded).expect_err("padded spelling must be rejected"); + assert!(err.contains("Non-canonical"), "unexpected error: {err}"); + } + + #[test] + fn test_from_b58_rejects_non_canonical_trailing_zero_limb_value() { + // Limb 4 equal to the prime is out of the canonical limb range even + // though it fits in a u64: to_b58 spells it as six limbs, so the + // re-encode check must reject it. + let non_canonical = Hash { + values: [0, 0, 0, 0, 0xFFFF_FFFF_0000_0001], + } + .to_b58(); + assert!(Hash::from_b58(&non_canonical).is_err()); + } } diff --git a/tx-types/src/inspect.rs b/tx-types/src/inspect.rs index 76e29bb..567c090 100644 --- a/tx-types/src/inspect.rs +++ b/tx-types/src/inspect.rs @@ -8,6 +8,7 @@ use crate::transaction_types_v1::{ SeedV1, SeedsV1, SpendCondition, SpendsV1, Witness, }; use nockapp::noun::slab::NounSlab; +use nockvm::noun::NounAllocator; use noun_serde::NounDecode; #[derive(Debug, Clone, Copy)] @@ -243,7 +244,8 @@ fn decode_lock_entry(untyped: &UntypedNoun) -> Value { } }; - let cell = match noun.as_cell() { + let space = slab.noun_space(); + let cell = match noun.in_space(&space).as_cell() { Ok(cell) => cell, Err(_) => { return json!({ @@ -260,7 +262,7 @@ fn decode_lock_entry(untyped: &UntypedNoun) -> Value { .and_then(|atom| atom.as_u64().ok()) .unwrap_or(0); - match SpendCondition::from_noun(&cell.tail()) { + match SpendCondition::from_noun(&cell.tail().noun(), &space) { Ok(cond) => json!({ "type": "lock", "version": version, diff --git a/tx-types/src/lib.rs b/tx-types/src/lib.rs index f529974..e728da0 100644 --- a/tx-types/src/lib.rs +++ b/tx-types/src/lib.rs @@ -2,6 +2,52 @@ extern crate alloc; +#[cfg(all(feature = "embedded", feature = "native"))] +compile_error!("features `embedded` and `native` are mutually exclusive"); + +#[cfg(all(feature = "std", not(feature = "embedded"), not(feature = "native")))] +compile_error!("feature `std` requires either `embedded` or `native`"); + +// The same source is compiled against either the vendored minimal runtime or +// the upstream nockchain crates. Aliasing the dependencies onto their canonical +// names keeps every `::nockvm` / `::noun_serde` path (including derive output) +// resolving identically for both. +// ibig is needed by the crypto-only no_std path as well, where no Nockchain +// backend is selected. +#[cfg(not(feature = "native"))] +extern crate ibig_minimal as ibig; +#[cfg(feature = "embedded")] +extern crate nockapp_minimal as nockapp; +#[cfg(feature = "embedded")] +extern crate nockchain_libp2p_io_minimal as nockchain_libp2p_io; +#[cfg(feature = "embedded")] +extern crate nockchain_math_minimal as nockchain_math; +#[cfg(feature = "embedded")] +extern crate nockvm_minimal as nockvm; +#[cfg(feature = "embedded")] +extern crate nockvm_macros_minimal as nockvm_macros; +#[cfg(feature = "embedded")] +extern crate noun_serde_minimal as noun_serde; +#[cfg(feature = "embedded")] +extern crate zkvm_jetpack_minimal as zkvm_jetpack; + +#[cfg(feature = "native")] +extern crate ibig_upstream as ibig; +#[cfg(feature = "native")] +extern crate nockapp_upstream as nockapp; +#[cfg(feature = "native")] +extern crate nockchain_libp2p_io_upstream as nockchain_libp2p_io; +#[cfg(feature = "native")] +extern crate nockchain_math_upstream as nockchain_math; +#[cfg(feature = "native")] +extern crate nockvm_upstream as nockvm; +#[cfg(feature = "native")] +extern crate nockvm_macros_upstream as nockvm_macros; +#[cfg(feature = "native")] +extern crate noun_serde_upstream as noun_serde; +#[cfg(feature = "native")] +extern crate zkvm_jetpack_upstream as zkvm_jetpack; + pub use pokenoun; // Crypto module is always available (no_std compatible) diff --git a/tx-types/src/signer.rs b/tx-types/src/signer.rs index f511ac6..8688ba0 100644 --- a/tx-types/src/signer.rs +++ b/tx-types/src/signer.rs @@ -56,7 +56,7 @@ pub fn schnorr_sign_digest(secret_key: T8, public_key: SchnorrPubkey, message: H &message.values[..], // 5 elements &secret_key.values[..], // 8 elements (T8 format secret key) ]) - .unwrap_or_else(|_| Hash { values: [0; 5] }); + .expect("schnorr nonce transcript limbs are u64"); let nonce_be = trunc_g_order_to_be32(nonce_digest.values); @@ -77,7 +77,7 @@ pub fn schnorr_sign_digest(secret_key: T8, public_key: SchnorrPubkey, message: H &public_key.y.values[..], // pubkey.y (6 elements) &message.values[..], // message (5 elements) ]) - .unwrap_or_else(|_| Hash { values: [0; 5] }); + .expect("schnorr challenge transcript limbs are u64"); let chal_be = trunc_g_order_to_be32(chal_digest.values); // Verify challenge != 0 (line 1648) diff --git a/tx-types/src/tests/test_empty_hashes.rs b/tx-types/src/tests/test_empty_hashes.rs index 0d6b12a..ae5d9c8 100644 --- a/tx-types/src/tests/test_empty_hashes.rs +++ b/tx-types/src/tests/test_empty_hashes.rs @@ -8,13 +8,14 @@ mod test_empty { // Test empty NName let empty_name = NName { p: vec![] }; let name_hashable = empty_name.to_hashable(); - let name_hash = hash_hashable(&name_hashable); + let name_hash = hash_hashable(&name_hashable).expect("NName hashable is u64-based"); println!("Empty NName hash: {:x?}", name_hash.values); // Test empty timelock let empty_timelock = Timelock { intent: None }; let timelock_hashable = empty_timelock.to_hashable(); - let timelock_hash = hash_hashable(&timelock_hashable); + let timelock_hash = + hash_hashable(&timelock_hashable).expect("timelock hashable is u64-based"); println!("Empty Timelock hash: {:x?}", timelock_hash.values); // Both should be the same (null/0 hashes) diff --git a/tx-types/src/tests/test_signature_hashable.rs b/tx-types/src/tests/test_signature_hashable.rs index 1b60eca..5f9252b 100644 --- a/tx-types/src/tests/test_signature_hashable.rs +++ b/tx-types/src/tests/test_signature_hashable.rs @@ -274,10 +274,19 @@ mod test_signature_hashable { // Hash both and compare let rust_hash = match rust_hashable { Hashable::Hash(h) => h, - _ => hash_hashable(&rust_hashable), + _ => hash_hashable(&rust_hashable).expect("signature hashable is u64-based"), }; - let expected_hash_from_hashable = hash_hashable(&expected_hashable); + let expected_hash_from_hashable = match hash_hashable(&expected_hashable) { + Ok(hash) => hash, + Err(err) => { + // This fixture packs the whole 512-bit signature into one leaf + // atom, which is not belt-based; there is no valid TIP5 digest + // for it. The test is diagnostic (it only prints), so skip. + println!("Manually constructed hashable is unbased: {err}"); + return; + } + }; println!("\nRust Signature hash: {:016x?}", rust_hash.values); println!( diff --git a/tx-types/src/tests/test_zmap_structure.rs b/tx-types/src/tests/test_zmap_structure.rs index c5a61f8..7f18cab 100644 --- a/tx-types/src/tests/test_zmap_structure.rs +++ b/tx-types/src/tests/test_zmap_structure.rs @@ -26,9 +26,12 @@ mod tests { use crate::hashing::tip5::Tip5Hasher; use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + let mut slab: NounSlab = NounSlab::new(); let noun = self.to_noun(&mut slab); - Tip5Hasher::hash_noun_varlen(noun).unwrap_or_else(|_| Hash { values: [0; 5] }) + let space = slab.noun_space(); + Tip5Hasher::hash_noun_varlen(noun, &space).unwrap_or_else(|_| Hash { values: [0; 5] }) } } @@ -92,7 +95,7 @@ mod tests { ); // Compute the final hash - let map_hash = hash_hashable(&hashable); + let map_hash = hash_hashable(&hashable).expect("z-map hashable is u64-based"); println!("Final ZMap hash: {:016x?}", map_hash.values); // Verify we have all 10 elements diff --git a/tx-types/src/tests/test_zset_simple.rs b/tx-types/src/tests/test_zset_simple.rs index 7c943a4..3bfd8df 100644 --- a/tx-types/src/tests/test_zset_simple.rs +++ b/tx-types/src/tests/test_zset_simple.rs @@ -25,9 +25,12 @@ mod tests { use crate::hashing::tip5::Tip5Hasher; use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + let mut slab: NounSlab = NounSlab::new(); let noun = self.to_noun(&mut slab); - Tip5Hasher::hash_noun_varlen(noun).unwrap_or_else(|_| Hash { values: [0; 5] }) + let space = slab.noun_space(); + Tip5Hasher::hash_noun_varlen(noun, &space).unwrap_or_else(|_| Hash { values: [0; 5] }) } } @@ -83,7 +86,7 @@ mod tests { Hashable::Hash(hash) }); - let set_hash = hash_hashable(&hashable); + let set_hash = hash_hashable(&hashable).expect("z-set hashable is u64-based"); println!("Final ZSet hash: {:016x?}", set_hash.values); // Verify we have 2 elements @@ -157,7 +160,7 @@ mod tests { Hashable::Hash(hash) }); - let set_hash = hash_hashable(&hashable); + let set_hash = hash_hashable(&hashable).expect("z-set hashable is u64-based"); println!("Final ZSet hash: {:016x?}", set_hash.values); // Verify we have 3 elements diff --git a/tx-types/src/tests/test_zset_structure.rs b/tx-types/src/tests/test_zset_structure.rs index 721f5ff..cb369b6 100644 --- a/tx-types/src/tests/test_zset_structure.rs +++ b/tx-types/src/tests/test_zset_structure.rs @@ -25,9 +25,12 @@ mod tests { use crate::hashing::tip5::Tip5Hasher; use nockapp::noun::slab::NounSlab; + use nockvm::noun::NounAllocator; + let mut slab: NounSlab = NounSlab::new(); let noun = self.to_noun(&mut slab); - Tip5Hasher::hash_noun_varlen(noun).unwrap_or_else(|_| Hash { values: [0; 5] }) + let space = slab.noun_space(); + Tip5Hasher::hash_noun_varlen(noun, &space).unwrap_or_else(|_| Hash { values: [0; 5] }) } } @@ -84,7 +87,7 @@ mod tests { }); // Compute the final hash - let set_hash = hash_hashable(&hashable); + let set_hash = hash_hashable(&hashable).expect("z-set hashable is u64-based"); println!("Final ZSet hash: {:016x?}", set_hash.values); // Print the tree structure diff --git a/tx-types/src/transaction_types.rs b/tx-types/src/transaction_types.rs index fb59820..1d91db8 100644 --- a/tx-types/src/transaction_types.rs +++ b/tx-types/src/transaction_types.rs @@ -1,14 +1,14 @@ use nockapp::noun::slab::NounSlab; use nockapp::noun::AtomExt; use nockapp::Noun; -use nockvm::noun::{Atom, NounAllocator, D, T}; +use nockvm::noun::{Atom, NounAllocator, NounSpace, D, T}; use noun_serde::{NounDecode, NounDecodeError, NounEncode}; use std::collections::HashMap; use crate::collections::{ZMap, ZSet}; use crate::hashing::hashable::Hashable; use crate::hashing::hasher::hash_hashable; -use crate::hashing::tip5::Tip5Hasher; +use crate::hashing::tip5::{Tip5Error, Tip5Hasher}; use crate::transaction_types_v0::*; use crate::transaction_types_v1::*; @@ -116,23 +116,40 @@ impl Hash { /// Convert base58 encoded string to Hash /// Implements the Hoon from-b58 function: /// ++ from-b58 |=(=cord `form`(atom-to-digest:tip5 (de-base58 (trip cord)))) + /// + /// Decoding requires exactly five base-p limbs (so the value is in the + /// tip5 domain) and the canonical base58 spelling. Values with a non-zero + /// sixth quotient and aliases such as a leading `1` (zero byte) are + /// rejected instead of being silently truncated or re-encoded. pub fn from_b58(base58: &str) -> Result { use crate::hashing::u320::U320; - let n = U320::from_base58(base58).map_err(|e| format!("Invalid base58: {}", e))?; + let mut quotient = + U320::from_base58(base58).map_err(|e| format!("Invalid base58: {}", e))?; - // Four divmods by p: collect remainders a..d; final quotient e - let (q1, a) = n.divrem_p(); - let (q2, b) = q1.divrem_p(); - let (q3, c) = q2.divrem_p(); - let (e_q, d) = q3.divrem_p(); - let e = e_q - .as_single_u64() - .map_err(|e| format!("Hash value too large: {}", e))?; + // Exactly five divmods by p. Anything left over after five limbs is + // outside the tip5 digest domain. + let mut values = [0u64; 5]; + for value in &mut values { + let (next, remainder) = quotient.divrem_p(); + *value = remainder; + quotient = next; + } - Ok(Hash { - values: [a, b, c, d, e], - }) + if quotient.as_single_u64().unwrap_or(1) != 0 { + return Err("Hash value too large: value does not fit in five base-p limbs".into()); + } + + let hash = Hash { values }; + + // Reject non-canonical spellings (for example a leading '1' zero + // byte) that decode to the same limbs. Without this check distinct + // strings would alias the same digest. + if hash.to_b58() != base58 { + return Err("Non-canonical base58 encoding".into()); + } + + Ok(hash) } /// Convert Hash to base58 encoded string @@ -201,7 +218,7 @@ impl NName { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("NName hashable is u64-based") } // TODO: implement v1 hashing @@ -234,12 +251,10 @@ impl NName { Hashable::Hash(owners.to_hash()), Hashable::null(), ]); - hash_hashable(&hashable) + hash_hashable(&hashable).expect("first_v0 hashable is u64-based") } - /// Compute the last hash component of an NName /// Based on Hoon implementation: - /// |= [=source =timelock] /// %- hash-hashable:tip5 /// :* leaf+& :: outcome of second pact /// (hashable:^source source) :: source of note @@ -253,7 +268,7 @@ impl NName { Hashable::Hash(timelock.to_hash()), Hashable::null(), ]); - hash_hashable(&hashable) + hash_hashable(&hashable).expect("last_v0 hashable is u64-based") } pub fn new_v1(lock: Hash, source: Source) -> Self { @@ -267,6 +282,7 @@ impl NName { // & (loobean true/yes) = 0, so leaf+& = leaf+0 = Hashable::null() let loobean_yes = Hashable::null(); hash_hashable(&Hashable::cell(loobean_yes, Hashable::Hash(lock))) + .expect("first_v1 hashable is u64-based") } pub fn last_v1(source: Source) -> Hash { @@ -278,6 +294,7 @@ impl NName { source.to_hashable(), Hashable::null(), ])) + .expect("last_v1 hashable is u64-based") } pub fn display(&self) -> Vec { @@ -296,7 +313,7 @@ impl Timelock { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("timelock hashable is u64-based") } } @@ -354,8 +371,8 @@ impl NounEncode for Timelock { } impl NounDecode for Timelock { - fn from_noun(noun: &Noun) -> Result { - let intent = TimelockIntent::from_noun(noun)?; + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + let intent = TimelockIntent::from_noun(noun, space)?; Timelock::new(intent).map_err(|e| NounDecodeError::Custom(e)) } } @@ -387,7 +404,7 @@ impl TimelockRange { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("timelock-range hashable is u64-based") } } // F6LT is a 6-element field type (a0-a5 in Hoon) @@ -406,13 +423,14 @@ impl NounEncode for F6LT { } impl NounDecode for F6LT { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // Extract 6 values from nested cell structure let mut values = [0u64; 6]; let mut current = *noun; for i in 0..5 { let cell = current + .in_space(space) .as_cell() .map_err(|_| NounDecodeError::ExpectedCell)?; values[i] = cell @@ -420,11 +438,12 @@ impl NounDecode for F6LT { .as_atom() .map_err(|_| NounDecodeError::ExpectedAtom)? .as_u64()?; - current = cell.tail(); + current = cell.tail().noun(); } // Last element values[5] = current + .in_space(space) .as_atom() .map_err(|_| NounDecodeError::ExpectedAtom)? .as_u64()?; @@ -463,7 +482,7 @@ impl SchnorrPubkey { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("schnorr-pubkey hashable is u64-based") } /// Convert base58 encoded string to SchnorrPubkey @@ -676,8 +695,9 @@ impl Lock { // Hash each pubkey let mut slab: NounSlab = NounSlab::new(); let pubkey_noun = pubkey.to_noun(&mut slab); - let pubkey_hash = Tip5Hasher::hash_noun_varlen(pubkey_noun) - .unwrap_or_else(|_| Hash { values: [0; 5] }); + let space = slab.noun_space(); + let pubkey_hash = Tip5Hasher::hash_noun_varlen(pubkey_noun, &space) + .expect("schnorr pubkey noun is u64-based"); Hashable::Hash(pubkey_hash) }); @@ -689,7 +709,7 @@ impl Lock { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("lock hashable is u64-based") } /// Convert base58 encoded multisig parameters to Lock @@ -782,7 +802,7 @@ impl Source { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("source hashable is u64-based") } } @@ -794,20 +814,20 @@ pub enum NNote { } impl NNote { - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { match self { - NNote::V0(v0) => v0.to_hash(), + NNote::V0(v0) => Ok(v0.to_hash()), NNote::V1(v1) => v1.to_hash(), } } } impl NounDecode for NNote { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = NNoteV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = NNoteV0::from_noun(noun, space) { return Ok(NNote::V0(v0)); } - if let Ok(v1) = NNoteV1::from_noun(noun) { + if let Ok(v1) = NNoteV1::from_noun(noun, space) { return Ok(NNote::V1(v1)); } Err(NounDecodeError::Custom( @@ -849,7 +869,7 @@ impl SchnorrSignature { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("schnorr-signature hashable is u64-based") } } // signature structure @@ -895,14 +915,14 @@ impl Spend { /// %1 [leaf+%1 (hashable:spend-1 +.form)] /// == /// ``` - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { let version_leaf = Hashable::leaf_from_atom(&self.version.to_le_bytes()); // Pair with the body's hashable: [version-leaf body-hashable] - Hashable::cell(version_leaf, self.body.to_hashable()) + Ok(Hashable::cell(version_leaf, self.body.to_hashable()?)) } - pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + pub fn to_hash(&self) -> Result { + hash_hashable(&self.to_hashable()?) } } @@ -924,17 +944,17 @@ impl NounEncode for SpendBody { } impl NounDecode for SpendBody { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // Try V0 first - if let Ok(v0) = SpendV0::from_noun(noun) { + if let Ok(v0) = SpendV0::from_noun(noun, space) { return Ok(SpendBody::V0(v0)); } // Try V0ToV1 - if let Ok(v0tov1) = SpendV0ToV1::from_noun(noun) { + if let Ok(v0tov1) = SpendV0ToV1::from_noun(noun, space) { return Ok(SpendBody::V0ToV1(v0tov1)); } // Try V1 - if let Ok(v1) = SpendV1::from_noun(noun) { + if let Ok(v1) = SpendV1::from_noun(noun, space) { return Ok(SpendBody::V1(v1)); } Err(NounDecodeError::Custom( @@ -944,19 +964,19 @@ impl NounDecode for SpendBody { } impl SpendBody { - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { match self { - SpendBody::V0(v) => v.to_hashable(), + SpendBody::V0(v) => Ok(v.to_hashable()), SpendBody::V0ToV1(v) => v.to_hashable(), SpendBody::V1(v) => v.to_hashable(), } } - pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + pub fn to_hash(&self) -> Result { + hash_hashable(&self.to_hashable()?) } - pub fn sig_hash(&self) -> Hash { + pub fn sig_hash(&self) -> Result { match self { - SpendBody::V0(v) => v.sig_hash(), + SpendBody::V0(v) => Ok(v.sig_hash()), SpendBody::V0ToV1(v) => v.compute_sig_hash(), SpendBody::V1(v) => v.compute_sig_hash(), } @@ -980,8 +1000,8 @@ impl NounEncode for Input { } impl NounDecode for Input { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = InputV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = InputV0::from_noun(noun, space) { return Ok(Input::V0(v0)); } Err(NounDecodeError::Custom( @@ -998,7 +1018,7 @@ impl Input { } } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("input hashable is u64-based") } pub fn calculate_timelock_range(&self) -> (Option, Option) { match self { @@ -1025,8 +1045,8 @@ impl NounEncode for Inputs { } impl NounDecode for Inputs { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = InputsV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = InputsV0::from_noun(noun, space) { return Ok(Inputs::V0(v0)); } Err(NounDecodeError::Custom( @@ -1043,7 +1063,7 @@ impl Inputs { } } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("inputs hashable is u64-based") } } @@ -1063,9 +1083,10 @@ pub enum Tx { } impl NounDecode for Tx { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // First, peek at the version field (first field of the struct) let cell = noun + .in_space(space) .as_cell() .map_err(|_| NounDecodeError::Custom("Tx must be a cell".into()))?; @@ -1080,8 +1101,8 @@ impl NounDecode for Tx { // Dispatch based on version match version { - 0 => TxV0::from_noun(noun).map(Tx::V0), - 1 => TxV1::from_noun(noun).map(Tx::V1), + 0 => TxV0::from_noun(noun, space).map(Tx::V0), + 1 => TxV1::from_noun(noun, space).map(Tx::V1), _ => Err(NounDecodeError::Custom(format!( "Tx enum decode: unsupported version {}", version @@ -1106,8 +1127,8 @@ impl NounEncode for Outputs { } impl NounDecode for Outputs { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = OutputsV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = OutputsV0::from_noun(noun, space) { return Ok(Outputs::V0(v0)); } Err(NounDecodeError::Custom( @@ -1132,8 +1153,8 @@ impl NounEncode for Output { } impl NounDecode for Output { - fn from_noun(noun: &Noun) -> Result { - if let Ok(v0) = OutputV0::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(v0) = OutputV0::from_noun(noun, space) { return Ok(Output::V0(v0)); } Err(NounDecodeError::Custom( @@ -1156,13 +1177,13 @@ pub enum RawTransaction { } impl NounDecode for RawTransaction { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // Try V1 first (has version field) - if let Ok(v1) = RawTransactionV1::from_noun(noun) { + if let Ok(v1) = RawTransactionV1::from_noun(noun, space) { return Ok(RawTransaction::V1(v1)); } // Fall back to V0 - if let Ok(v0) = RawTransactionV0::from_noun(noun) { + if let Ok(v0) = RawTransactionV0::from_noun(noun, space) { return Ok(RawTransaction::V0(v0)); } Err(NounDecodeError::Custom( @@ -1172,19 +1193,23 @@ impl NounDecode for RawTransaction { } impl NounDecode for T8 { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let mut ret: [u64; 8] = [0; 8]; let mut cur = *noun; for i in 0..7 { - let cur_cell = cur.as_cell().map_err(|_| NounDecodeError::ExpectedCell)?; + let cur_cell = cur + .in_space(space) + .as_cell() + .map_err(|_| NounDecodeError::ExpectedCell)?; ret[i] = cur_cell .head() .as_atom() .map_err(|_| NounDecodeError::ExpectedAtom)? .as_u64()?; - cur = cur_cell.tail(); + cur = cur_cell.tail().noun(); } ret[7] = cur + .in_space(space) .as_atom() .map_err(|_| NounDecodeError::ExpectedAtom)? .as_u64()?; diff --git a/tx-types/src/transaction_types_v0.rs b/tx-types/src/transaction_types_v0.rs index 7172861..d79b845 100644 --- a/tx-types/src/transaction_types_v0.rs +++ b/tx-types/src/transaction_types_v0.rs @@ -29,7 +29,7 @@ impl NNoteV0 { let hashable = self.to_hashable(); // Hash the structure - hash_hashable(&hashable) + hash_hashable(&hashable).expect("v0 hashable is u64-based") } pub fn to_hashable(&self) -> Hashable { @@ -40,16 +40,27 @@ impl NNoteV0 { Hashable::triple( Hashable::leaf_from_atom(&self.meta.version.to_le_bytes()), self.meta.origin_page.to_hashable(), - Hashable::Hash(hash_hashable(&self.meta.timelock.to_hashable())), + Hashable::Hash( + hash_hashable(&self.meta.timelock.to_hashable()) + .expect("v0 timelock hashable is u64-based"), + ), ), // Second part: [name-hash lock-hash source-hash assets] // Note: In Hoon this is a quad (4-element structure) Hashable::cell( - Hashable::Hash(hash_hashable(&self.name.to_hashable())), + Hashable::Hash( + hash_hashable(&self.name.to_hashable()).expect("v0 name hashable is u64-based"), + ), Hashable::cell( - Hashable::Hash(hash_hashable(&self.lock.to_hashable())), + Hashable::Hash( + hash_hashable(&self.lock.to_hashable()) + .expect("v0 lock hashable is u64-based"), + ), Hashable::cell( - Hashable::Hash(hash_hashable(&self.source.to_hashable())), + Hashable::Hash( + hash_hashable(&self.source.to_hashable()) + .expect("v0 source hashable is u64-based"), + ), self.assets.to_hashable(), ), ), @@ -91,7 +102,7 @@ impl SeedV0 { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based") } pub fn to_sig_hashable(&self) -> Hashable { @@ -155,7 +166,7 @@ impl SeedsV0 { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based") } } @@ -176,7 +187,7 @@ impl Signature { pub fn to_hash(&self) -> Hash { match self.to_hashable() { Hashable::Hash(h) => h, - _ => hash_hashable(&self.to_hashable()), + _ => hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based"), } } } @@ -201,12 +212,12 @@ impl SpendV0 { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based") } pub fn sig_hash(&self) -> Hash { let sig_hashable = Hashable::cell(self.seeds.to_sig_hashable(), self.fee.to_hashable()); - hash_hashable(&sig_hashable) + hash_hashable(&sig_hashable).expect("v0 signature hashable is u64-based") } } @@ -222,7 +233,7 @@ impl InputV0 { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based") } pub fn calculate_timelock_range(&self) -> (Option, Option) { @@ -285,7 +296,7 @@ impl InputsV0 { } pub fn to_hash(&self) -> Hash { - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("v0 hashable is u64-based") } } diff --git a/tx-types/src/transaction_types_v1.rs b/tx-types/src/transaction_types_v1.rs index 6751592..45f9951 100644 --- a/tx-types/src/transaction_types_v1.rs +++ b/tx-types/src/transaction_types_v1.rs @@ -4,125 +4,122 @@ use crate::collections::zset::DorTip as ZSetDorTip; use crate::collections::{ZMap, ZSet}; use crate::generic_noun::UntypedNoun; use crate::transaction_types::*; -use nockapp::noun::slab::{CueError, NounSlab}; +use nockapp::noun::slab::NounSlab; use nockapp::utils::make_tas; use nockapp::Bytes; -use nockvm::noun::{Atom, Noun, D}; +use nockvm::noun::{Noun, NounAllocator, NounSpace, D}; use once_cell::sync::Lazy; use crate::hashing::hashable::Hashable; use crate::hashing::hasher::{hash_hashable, hash_ten_cell}; -use crate::hashing::raw::{ - compute_tx_id_v1_from_raw_noun, hashable_note_data_from_raw, hashable_pkh_signature_direct, - hashable_pkh_signature_from_raw, hashable_seeds_from_raw, hashable_spend_from_raw, - hashable_spends_from_raw, hashable_witness_from_raw, -}; -use crate::hashing::tip5::Tip5Hasher; +use crate::hashing::raw::hashable_spends_from_raw; +use crate::hashing::tip5::{Tip5Error, Tip5Hasher}; static LOCK_MERKLE_AXIS_HASH: Lazy = Lazy::new(|| { Hash::from_b58("6mhCSwJQDvbkbiPAUNjetJtVoo1VLtEhmEYoU4hmdGd6ep1F6ayaV4A") .expect("valid lock merkle proof axis hash constant") }); -fn hashable_from_noun_recursive(noun: nockvm::noun::Noun) -> Hashable { - if let Ok(cell) = noun.as_cell() { - Hashable::cell( - hashable_from_noun_recursive(cell.head()), - hashable_from_noun_recursive(cell.tail()), - ) +fn hashable_from_noun_recursive( + noun: nockvm::noun::Noun, + space: &NounSpace, +) -> Result { + if let Ok(cell) = noun.in_space(space).as_cell() { + Ok(Hashable::cell( + hashable_from_noun_recursive(cell.head().noun(), space)?, + hashable_from_noun_recursive(cell.tail().noun(), space)?, + )) } else { - let atom = noun - .as_atom() - .expect("hashable_from_noun_recursive: expected atom or cell"); + let atom = noun.in_space(space).as_atom().map_err(|_| { + Tip5Error::InvalidNoun("hashable_from_noun_recursive: expected atom or cell".into()) + })?; match atom.as_u64() { - Ok(val) => Hashable::leaf_from_atom(&val.to_le_bytes()), + Ok(val) => Ok(Hashable::leaf_from_atom(&val.to_le_bytes())), Err(_) => { let mut slab: NounSlab = NounSlab::new(); - slab.copy_into(atom.as_noun()); - Hashable::Leaf(slab.jam().to_vec()) + slab.copy_into(atom.as_noun().noun(), space); + Ok(Hashable::Leaf(slab.jam().to_vec())) } } } } -fn hashable_from_untyped_noun(untyped: &UntypedNoun) -> Hashable { +fn hashable_from_untyped_noun(untyped: &UntypedNoun) -> Result { let mut slab: NounSlab = NounSlab::new(); - let noun = match slab.cue_into(untyped.p.clone()) { - Ok(noun) => noun, - Err(_) => return Hashable::Leaf(untyped.p.to_vec()), - }; + let noun = slab + .cue_into(untyped.p.clone()) + .map_err(|e| Tip5Error::SerializationError(format!("failed to cue noun: {:?}", e)))?; - hashable_from_noun_recursive(noun) + let space = slab.noun_space(); + hashable_from_noun_recursive(noun, &space) } -fn hashable_leaf_from_noun(noun: Noun) -> Hashable { +fn hashable_leaf_from_noun(noun: Noun, space: &NounSpace) -> Hashable { let mut slab: NounSlab = NounSlab::new(); - let copy = slab.copy_into(noun); + let copy = slab.copy_into(noun, space); slab.set_root(copy); Hashable::Leaf(slab.jam().to_vec()) } -fn hashable_note_data_from_zmap(noun: Noun) -> Hashable { - if let Ok(atom) = noun.as_atom() { +fn hashable_note_data_from_zmap(noun: Noun, space: &NounSpace) -> Result { + if let Ok(atom) = noun.in_space(space).as_atom() { if atom.as_u64().unwrap_or(1) == 0 { - return Hashable::null(); + return Ok(Hashable::null()); } } - let cell = noun.as_cell().expect("note-data node should be a cell"); + let invalid = || Tip5Error::InvalidNoun("malformed note-data node".into()); + let cell = noun.in_space(space).as_cell().map_err(|_| invalid())?; let node = cell.head(); - let children = cell - .tail() - .as_cell() - .expect("note-data child tuple should be a cell"); + let children = cell.tail().as_cell().map_err(|_| invalid())?; let left = children.head(); let right = match children.tail().as_cell() { Ok(right_cell) => right_cell.head(), Err(_) => children.tail(), }; - let pair = node - .as_cell() - .expect("note-data key/value node should be a cell"); + let pair = node.as_cell().map_err(|_| invalid())?; let key_noun = pair.head(); let value_noun = pair.tail(); - let key_leaf = hashable_leaf_from_noun(key_noun); - let value_hashable = hashable_from_noun_recursive(value_noun); + let key_leaf = hashable_leaf_from_noun(key_noun.noun(), space); + let value_hashable = hashable_from_noun_recursive(value_noun.noun(), space)?; let node_hashable = Hashable::cell(key_leaf, value_hashable); - Hashable::triple( + Ok(Hashable::triple( node_hashable, - hashable_note_data_from_zmap(left), - hashable_note_data_from_zmap(right), - ) + hashable_note_data_from_zmap(left.noun(), space)?, + hashable_note_data_from_zmap(right.noun(), space)?, + )) } -pub fn hashable_spends_from_zmap(noun: Noun) -> Hashable { +pub fn hashable_spends_from_zmap(noun: Noun, space: &NounSpace) -> Result { if noun_is_null(noun) { - return Hashable::null(); + return Ok(Hashable::null()); } - let (node, left, right) = decompose_map(noun); - let (name_noun, spend_noun) = decompose_pair(node); + let (node, left, right) = decompose_map(noun, space)?; + let (name_noun, spend_noun) = decompose_pair(node, space)?; - let name = NName::from_noun(&name_noun).expect("failed to decode spend name"); - let spend = Spend::from_noun(&spend_noun).expect("failed to decode spend entry"); + let name = NName::from_noun(&name_noun, space) + .map_err(|e| Tip5Error::InvalidNoun(format!("failed to decode spend name: {:?}", e)))?; + let spend = Spend::from_noun(&spend_noun, space) + .map_err(|e| Tip5Error::InvalidNoun(format!("failed to decode spend entry: {:?}", e)))?; - let node_hashable = Hashable::cell(name.to_hashable(), spend.to_hashable()); + let node_hashable = Hashable::cell(name.to_hashable(), spend.to_hashable()?); - Hashable::triple( + Ok(Hashable::triple( node_hashable, - hashable_spends_from_zmap(left), - hashable_spends_from_zmap(right), - ) + hashable_spends_from_zmap(left, space)?, + hashable_spends_from_zmap(right, space)?, + )) } fn noun_is_null(noun: Noun) -> bool { unsafe { noun.raw_equals(&D(0)) } } -fn atom_trimmed_be(atom: Atom) -> Vec { +fn atom_trimmed_be(atom: nockvm::noun::AtomHandle) -> Vec { let mut bytes = atom.to_be_bytes(); let first_non_zero = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len()); if first_non_zero == bytes.len() { @@ -132,23 +129,30 @@ fn atom_trimmed_be(atom: Atom) -> Vec { } } -fn nouns_equal(a: Noun, b: Noun) -> bool { +fn nouns_equal(a: Noun, b: Noun, space: &NounSpace) -> bool { if unsafe { a.raw_equals(&b) } { return true; } - match (a.as_cell(), b.as_cell()) { - (Ok(ac), Ok(bc)) => nouns_equal(ac.head(), bc.head()) && nouns_equal(ac.tail(), bc.tail()), + match (a.in_space(space).as_cell(), b.in_space(space).as_cell()) { + (Ok(ac), Ok(bc)) => { + nouns_equal(ac.head().noun(), bc.head().noun(), space) + && nouns_equal(ac.tail().noun(), bc.tail().noun(), space) + } (Err(_), Err(_)) => { - let a_atom = a.as_atom().expect("noun should be atom"); - let b_atom = b.as_atom().expect("noun should be atom"); - atom_trimmed_be(a_atom) == atom_trimmed_be(b_atom) + if let (Ok(a_atom), Ok(b_atom)) = + (a.in_space(space).as_atom(), b.in_space(space).as_atom()) + { + atom_trimmed_be(a_atom) == atom_trimmed_be(b_atom) + } else { + false + } } _ => false, } } -fn atom_less_than(a: Atom, b: Atom) -> bool { +fn atom_less_than(a: nockvm::noun::AtomHandle, b: nockvm::noun::AtomHandle) -> bool { let a_bytes = atom_trimmed_be(a); let b_bytes = atom_trimmed_be(b); match a_bytes.len().cmp(&b_bytes.len()) { @@ -167,80 +171,88 @@ fn less_than_hash(a: &[u64; 5], b: &[u64; 5]) -> bool { hash_a.to_ubig() < hash_b.to_ubig() } -fn tip_hash(noun: Noun) -> [u64; 5] { - Tip5Hasher::hash_noun_varlen(noun) - .map(|hash| hash.values) - .unwrap_or([0; 5]) +fn tip_hash(noun: Noun, space: &NounSpace) -> Result<[u64; 5], Tip5Error> { + Tip5Hasher::hash_noun_varlen(noun, space).map(|hash| hash.values) } -fn double_tip_hash(noun: Noun) -> [u64; 5] { - let tip = tip_hash(noun); +fn double_tip_hash(noun: Noun, space: &NounSpace) -> Result<[u64; 5], Tip5Error> { + let tip = tip_hash(noun, space)?; let hash = Hash { values: tip }; - hash_ten_cell(hash.clone(), hash).values + Ok(hash_ten_cell(hash.clone(), hash).values) } -fn dor_tip_compare(a: Noun, b: Noun) -> bool { - if nouns_equal(a, b) { +fn dor_tip_compare(a: Noun, b: Noun, space: &NounSpace) -> bool { + if nouns_equal(a, b, space) { return true; } - match (a.as_cell(), b.as_cell()) { + match (a.in_space(space).as_cell(), b.in_space(space).as_cell()) { (Ok(ac), Ok(bc)) => { - if nouns_equal(ac.head(), bc.head()) { - dor_tip_compare(ac.tail(), bc.tail()) + if nouns_equal(ac.head().noun(), bc.head().noun(), space) { + dor_tip_compare(ac.tail().noun(), bc.tail().noun(), space) } else { - dor_tip_compare(ac.head(), bc.head()) + dor_tip_compare(ac.head().noun(), bc.head().noun(), space) } } (Ok(_), Err(_)) => false, (Err(_), Ok(_)) => false, (Err(_), Err(_)) => { - let a_atom = a.as_atom().expect("noun should be atom"); - let b_atom = b.as_atom().expect("noun should be atom"); - atom_less_than(a_atom, b_atom) + if let (Ok(a_atom), Ok(b_atom)) = + (a.in_space(space).as_atom(), b.in_space(space).as_atom()) + { + atom_less_than(a_atom, b_atom) + } else { + false + } } } } -fn gor_tip_compare(a: Noun, b: Noun) -> bool { - let a_tip = tip_hash(a); - let b_tip = tip_hash(b); +fn gor_tip_compare(a: Noun, b: Noun, space: &NounSpace) -> Result { + let a_tip = tip_hash(a, space)?; + let b_tip = tip_hash(b, space)?; if a_tip == b_tip { - dor_tip_compare(a, b) + Ok(dor_tip_compare(a, b, space)) } else { - less_than_hash(&a_tip, &b_tip) + Ok(less_than_hash(&a_tip, &b_tip)) } } -fn mor_tip_compare(a: Noun, b: Noun) -> bool { - let a_tip = double_tip_hash(a); - let b_tip = double_tip_hash(b); +fn mor_tip_compare(a: Noun, b: Noun, space: &NounSpace) -> Result { + let a_tip = double_tip_hash(a, space)?; + let b_tip = double_tip_hash(b, space)?; if a_tip == b_tip { - dor_tip_compare(a, b) + Ok(dor_tip_compare(a, b, space)) } else { - less_than_hash(&a_tip, &b_tip) + Ok(less_than_hash(&a_tip, &b_tip)) } } -fn decompose_map(noun: Noun) -> (Noun, Noun, Noun) { - let cell = noun.as_cell().expect("map node should be a cell"); - let node = cell.head(); +fn decompose_map(noun: Noun, space: &NounSpace) -> Result<(Noun, Noun, Noun), Tip5Error> { + let cell = noun + .in_space(space) + .as_cell() + .map_err(|_| Tip5Error::InvalidNoun("map node must be a cell".into()))?; + let node = cell.head().noun(); let tail = cell.tail(); if let Ok(children) = tail.as_cell() { - let left = children.head(); - let right = children.tail(); - (node, left, right) + let left = children.head().noun(); + let right = children.tail().noun(); + Ok((node, left, right)) } else { - (node, tail, D(0)) + Ok((node, tail.noun(), D(0))) } } -fn decompose_pair(noun: Noun) -> (Noun, Noun) { - let cell = noun.as_cell().expect("pair should be a cell"); - let key = cell.head(); - let value = cell.tail(); - (key, value) +fn decompose_pair(noun: Noun, space: &NounSpace) -> Result<(Noun, Noun), Tip5Error> { + let cell = noun + .in_space(space) + .as_cell() + .map_err(|_| Tip5Error::InvalidNoun("pair must be a cell".into()))?; + let key = cell.head().noun(); + let value = cell.tail().noun(); + Ok((key, value)) } fn canonical_zmap_put( @@ -248,50 +260,55 @@ fn canonical_zmap_put( map: Noun, key: Noun, value: Noun, -) -> Noun { +) -> Result { if noun_is_null(map) { let kv = nockvm::noun::T(alloc, &[key, value]); let children = nockvm::noun::T(alloc, &[D(0), D(0)]); - return nockvm::noun::T(alloc, &[kv, children]); + return Ok(nockvm::noun::T(alloc, &[kv, children])); } - let (node, left, right) = decompose_map(map); - let (node_key, node_value) = decompose_pair(node); + // Refresh the space after any prior allocation; a stack-backed space holds + // an epoch snapshot that allocations advance. + let space = alloc.noun_space(); + let (node, left, right) = decompose_map(map, &space)?; + let (node_key, node_value) = decompose_pair(node, &space)?; - if nouns_equal(key, node_key) { - if nouns_equal(value, node_value) { - map + if nouns_equal(key, node_key, &space) { + if nouns_equal(value, node_value, &space) { + Ok(map) } else { let new_node = nockvm::noun::T(alloc, &[key, value]); - nockvm::noun::T(alloc, &[new_node, left, right]) + Ok(nockvm::noun::T(alloc, &[new_node, left, right])) } - } else if gor_tip_compare(key, node_key) { - let new_left = canonical_zmap_put(alloc, left, key, value); - let (new_left_node, new_left_left, new_left_right) = decompose_map(new_left); - let (new_left_key, _) = decompose_pair(new_left_node); + } else if gor_tip_compare(key, node_key, &space)? { + let new_left = canonical_zmap_put(alloc, left, key, value)?; + let space = alloc.noun_space(); + let (new_left_node, new_left_left, new_left_right) = decompose_map(new_left, &space)?; + let (new_left_key, _) = decompose_pair(new_left_node, &space)?; - if mor_tip_compare(node_key, new_left_key) { + if mor_tip_compare(node_key, new_left_key, &space)? { let children = nockvm::noun::T(alloc, &[new_left, right]); - nockvm::noun::T(alloc, &[node, children]) + Ok(nockvm::noun::T(alloc, &[node, children])) } else { let right_children = nockvm::noun::T(alloc, &[new_left_right, right]); let new_right_branch = nockvm::noun::T(alloc, &[node, right_children]); let left_children = nockvm::noun::T(alloc, &[new_left_left, new_right_branch]); - nockvm::noun::T(alloc, &[new_left_node, left_children]) + Ok(nockvm::noun::T(alloc, &[new_left_node, left_children])) } } else { - let new_right = canonical_zmap_put(alloc, right, key, value); - let (new_right_node, new_right_left, new_right_right) = decompose_map(new_right); - let (new_right_key, _) = decompose_pair(new_right_node); + let new_right = canonical_zmap_put(alloc, right, key, value)?; + let space = alloc.noun_space(); + let (new_right_node, new_right_left, new_right_right) = decompose_map(new_right, &space)?; + let (new_right_key, _) = decompose_pair(new_right_node, &space)?; - if mor_tip_compare(node_key, new_right_key) { + if mor_tip_compare(node_key, new_right_key, &space)? { let children = nockvm::noun::T(alloc, &[left, new_right]); - nockvm::noun::T(alloc, &[node, children]) + Ok(nockvm::noun::T(alloc, &[node, children])) } else { let left_children = nockvm::noun::T(alloc, &[left, new_right_left]); let new_left_branch = nockvm::noun::T(alloc, &[node, left_children]); let right_children = nockvm::noun::T(alloc, &[new_left_branch, new_right_right]); - nockvm::noun::T(alloc, &[new_right_node, right_children]) + Ok(nockvm::noun::T(alloc, &[new_right_node, right_children])) } } } @@ -301,7 +318,7 @@ fn build_canonical_zmap_from_pairs( entries: I, mut key_fn: KF, mut value_fn: VF, -) -> Noun +) -> Result where A: nockvm::noun::NounAllocator, I: IntoIterator, @@ -312,15 +329,15 @@ where for (key, value) in entries { let key_noun = key_fn(alloc, key); let value_noun = value_fn(alloc, value); - root = canonical_zmap_put(alloc, root, key_noun, value_noun); + root = canonical_zmap_put(alloc, root, key_noun, value_noun)?; } - root + Ok(root) } fn build_canonical_note_data_noun( alloc: &mut A, map: &ZMap, -) -> Noun { +) -> Result { build_canonical_zmap_from_pairs( alloc, map.tap().into_iter(), @@ -332,7 +349,7 @@ fn build_canonical_note_data_noun( fn build_canonical_spends_noun( alloc: &mut A, map: &ZMap, -) -> Noun { +) -> Result { build_canonical_zmap_from_pairs( alloc, map.tap().into_iter(), @@ -358,12 +375,12 @@ pub struct NNoteV1 { } impl NNoteV1 { - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()?) } - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { /* :* leaf+version.form leaf+origin-page.form @@ -372,13 +389,13 @@ impl NNoteV1 { leaf+assets.form == */ - Hashable::cell_chain([ + Ok(Hashable::cell_chain([ Hashable::leaf_from_atom(&self.version.to_le_bytes()), self.origin_page.to_hashable(), Hashable::Hash(self.name.to_hash()), - Hashable::Hash(self.note_data.to_hash()), + Hashable::Hash(self.note_data.to_hash()?), self.assets.to_hashable(), - ]) + ])) } } @@ -390,28 +407,31 @@ pub struct NoteData { impl NounEncode for NoteData { fn to_noun(&self, alloc: &mut A) -> nockvm::noun::Noun { build_canonical_note_data_noun(alloc, &self.map) + .expect("note-data z-map keys are u64-based") } } impl NoteData { - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()?) } /// Compute hashable for note-data /// /// From Hoon, note-data is a z-map of @tas to * - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { let mut slab: NounSlab = NounSlab::new(); - let noun = build_canonical_note_data_noun(&mut slab, &self.map); - hashable_note_data_from_zmap(noun) + let noun = build_canonical_note_data_noun(&mut slab, &self.map)?; + let space = slab.noun_space(); + hashable_note_data_from_zmap(noun, &space) } /// Return the canonical note-data noun jam used for hashing/debugging. pub fn jam_canonical(&self) -> Vec { let mut slab: NounSlab = NounSlab::new(); - let noun = build_canonical_note_data_noun(&mut slab, &self.map); + let noun = build_canonical_note_data_noun(&mut slab, &self.map) + .expect("note-data z-map keys are u64-based"); slab.set_root(noun); slab.jam().to_vec() } @@ -441,7 +461,7 @@ impl SeedV1 { /// hash+parent-hash.sed /// == /// ``` - pub fn to_sig_hashable(&self) -> Hashable { + pub fn to_sig_hashable(&self) -> Result { use crate::hashing::hasher::hash_hashable; // Compute output_source hashable @@ -451,17 +471,17 @@ impl SeedV1 { }; // Hash the note_data - let note_data_hash = hash_hashable(&self.note_data.to_hashable()); + let note_data_hash = hash_hashable(&self.note_data.to_hashable()?)?; // Build the 5-element structure (quint) // Using nested cells: [a [b [c [d e]]]] - Hashable::cell_chain([ + Ok(Hashable::cell_chain([ output_source_hashable, Hashable::Hash(self.lock_root.clone()), Hashable::Hash(note_data_hash), self.gift.to_hashable(), Hashable::Hash(self.parent_hash.clone()), - ]) + ])) } /// Compute the regular hashable for a V1 seed (used in spend hashable, not signatures) @@ -476,15 +496,15 @@ impl SeedV1 { /// leaf+gift.sed /// hash+parent-hash.sed /// ``` - pub fn to_regular_hashable(&self) -> Hashable { + pub fn to_regular_hashable(&self) -> Result { use crate::hashing::hasher::hash_hashable; // Hash the note_data - let note_data_hash = hash_hashable(&self.note_data.to_hashable()); + let note_data_hash = hash_hashable(&self.note_data.to_hashable()?)?; // Build the 4-element structure (quad): [a b c d] // In Hoon, :^ creates a quad which is [a [b [c d]]] - Hashable::cell( + Ok(Hashable::cell( Hashable::Hash(self.lock_root.clone()), Hashable::cell( Hashable::Hash(note_data_hash), @@ -493,7 +513,7 @@ impl SeedV1 { Hashable::Hash(self.parent_hash.clone()), ), ), - ) + )) } } @@ -515,9 +535,9 @@ impl SeedsV1 { /// $(form l.form) /// $(form r.form) /// ``` - pub fn to_sig_hashable(&self) -> Hashable { - // Use ZSet's to_hashable method with sig_hashable for each seed - self.set.to_hashable(|seed| seed.to_sig_hashable()) + pub fn to_sig_hashable(&self) -> Result { + // Use ZSet's fallible hashable builder with sig_hashable for each seed + self.set.try_to_hashable(|seed| seed.to_sig_hashable()) } /// Compute the regular hashable for V1 seeds (used in spend hashable, not signatures) @@ -532,9 +552,9 @@ impl SeedsV1 { /// $(form l.form) /// $(form r.form) /// ``` - pub fn to_hashable(&self) -> Hashable { - // Use ZSet's to_hashable method with regular hashable for each seed - self.set.to_hashable(|seed| seed.to_regular_hashable()) + pub fn to_hashable(&self) -> Result { + // Use ZSet's fallible hashable builder with regular hashable for each seed + self.set.try_to_hashable(|seed| seed.to_regular_hashable()) } } @@ -583,10 +603,12 @@ impl SpendsV1 { /// $(form l.form) /// $(form r.form) /// ``` - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { let mut slab: NounSlab = NounSlab::new(); - let noun = build_canonical_spends_noun(&mut slab, &self.map); - hashable_spends_from_raw(noun).expect("invalid spends noun") + let noun = build_canonical_spends_noun(&mut slab, &self.map)?; + let space = slab.noun_space(); + hashable_spends_from_raw(noun, &space) + .map_err(|e| Tip5Error::InvalidNoun(format!("invalid spends noun: {:?}", e))) } /// Compute hash of spends @@ -598,15 +620,16 @@ impl SpendsV1 { /// %- hash-hashable:tip5 /// (hashable form) /// ``` - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()?) } /// Return the raw spends noun jam used for hashing/debugging. pub fn jam_canonical(&self) -> Vec { let mut slab: NounSlab = NounSlab::new(); - let noun = build_canonical_spends_noun(&mut slab, &self.map); + let noun = build_canonical_spends_noun(&mut slab, &self.map) + .expect("spend z-map keys are u64-based"); slab.set_root(noun); slab.jam().to_vec() } @@ -614,7 +637,7 @@ impl SpendsV1 { impl NounEncode for SpendsV1 { fn to_noun(&self, alloc: &mut A) -> Noun { - build_canonical_spends_noun(alloc, &self.map) + build_canonical_spends_noun(alloc, &self.map).expect("spend z-map keys are u64-based") } } @@ -623,27 +646,31 @@ fn version_leaf_hashable() -> Hashable { } /// Compute a tx-id from an already computed spends hashable structure. -fn compute_tx_id_from_spends_hashable(spends_hashable: Hashable) -> Hash { +fn compute_tx_id_from_spends_hashable(spends_hashable: Hashable) -> Result { let hashable = Hashable::cell(version_leaf_hashable(), spends_hashable); hash_hashable(&hashable) } /// Compute the tx-id from an in-memory spends map. -pub fn compute_tx_id_v1(spends: &SpendsV1) -> Hash { - compute_tx_id_from_spends_hashable(spends.to_hashable()) +pub fn compute_tx_id_v1(spends: &SpendsV1) -> Result { + compute_tx_id_from_spends_hashable(spends.to_hashable()?) } /// Compute the tx-id from the raw noun representation of the spends map. -pub fn compute_tx_id_v1_from_noun(spends_noun: Noun) -> Hash { - let spends = SpendsV1::from_noun(&spends_noun).expect("invalid spends noun"); +pub fn compute_tx_id_v1_from_noun(spends_noun: Noun, space: &NounSpace) -> Result { + let spends = SpendsV1::from_noun(&spends_noun, space) + .map_err(|e| Tip5Error::InvalidNoun(format!("invalid spends noun: {:?}", e)))?; compute_tx_id_v1(&spends) } /// Compute the tx-id directly from the jammed noun bytes of the spends map. -pub fn compute_tx_id_v1_from_jam(spends_jam: &[u8]) -> Result { +pub fn compute_tx_id_v1_from_jam(spends_jam: &[u8]) -> Result { let mut slab: NounSlab = NounSlab::new(); - let noun = slab.cue_into(Bytes::copy_from_slice(spends_jam))?; - Ok(compute_tx_id_v1_from_noun(noun)) + let noun = slab + .cue_into(Bytes::copy_from_slice(spends_jam)) + .map_err(|e| Tip5Error::SerializationError(format!("failed to cue spends: {:?}", e)))?; + let space = slab.noun_space(); + compute_tx_id_v1_from_noun(noun, &space) } #[derive(Debug, Clone, NounDecode, NounEncode)] @@ -675,10 +702,10 @@ impl SpendV1 { /// %- hash-hashable:tip5 /// [(sig-hashable:seeds seeds.sen) leaf+fee.sen] /// ``` - pub fn compute_sig_hash(&self) -> Hash { + pub fn compute_sig_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - let hashable = Hashable::cell(self.seeds.to_sig_hashable(), self.fee.to_hashable()); + let hashable = Hashable::cell(self.seeds.to_sig_hashable()?, self.fee.to_hashable()); hash_hashable(&hashable) } @@ -692,19 +719,19 @@ impl SpendV1 { /// ^- hashable:tip5 /// [(hashable:witness witness.sen) (hashable:seeds seeds.sen) leaf+fee.sen] /// ``` - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { // Build the triple: [witness-hashable seeds-hashable fee-leaf] // Note: seeds hashable uses regular hashable, not sig-hashable - Hashable::triple( - self.witness.to_hashable(), - self.seeds.to_hashable(), + Ok(Hashable::triple( + self.witness.to_hashable()?, + self.seeds.to_hashable()?, self.fee.to_hashable(), - ) + )) } - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()?) } } @@ -730,10 +757,10 @@ impl SpendV0ToV1 { /// %- hash-hashable:tip5 /// [(sig-hashable:seeds seeds.form) leaf+fee.form] /// ``` - pub fn compute_sig_hash(&self) -> Hash { + pub fn compute_sig_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - let hashable = Hashable::cell(self.seeds.to_sig_hashable(), self.fee.to_hashable()); + let hashable = Hashable::cell(self.seeds.to_sig_hashable()?, self.fee.to_hashable()); hash_hashable(&hashable) } @@ -747,7 +774,7 @@ impl SpendV0ToV1 { /// ^- hashable:tip5 /// [(hashable:signature:v0 signature.form) (hashable:seeds seeds.form) leaf+fee.form] /// ``` - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { // For V0 signatures, we need to traverse the ZMap properly let sig_hashable = self.signature.to_hashable( |pubkey| Hashable::Hash(pubkey.to_hash()), @@ -755,27 +782,12 @@ impl SpendV0ToV1 { ); // For seeds, use the regular hashable (not sig_hashable) - let seeds_hashable = self.seeds.set.to_hashable(|seed| { - // Regular hashable for seed (without output_source) - use crate::hashing::hasher::hash_hashable; - let note_data_hash = hash_hashable(&seed.note_data.to_hashable()); - - Hashable::cell( - Hashable::Hash(seed.lock_root.clone()), - Hashable::cell( - Hashable::Hash(note_data_hash), - Hashable::cell( - seed.gift.to_hashable(), - Hashable::Hash(seed.parent_hash.clone()), - ), - ), - ) - }); + let seeds_hashable = self.seeds.to_hashable()?; - Hashable::cell( + Ok(Hashable::cell( sig_hashable, Hashable::cell(seeds_hashable, self.fee.to_hashable()), - ) + )) } } @@ -849,7 +861,7 @@ impl ZSetDorTip for OutputV1 { } } -#[derive(Debug, Clone, NounDecode, NounEncode)] +#[derive(Debug, Clone, NounEncode)] pub struct Witness { pub lmp: LockMerkleProof, pub pkh: PkhSignature, @@ -857,6 +869,45 @@ pub struct Witness { pub tim: u64, } +impl NounDecode for Witness { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + use noun_serde::NounDecodeError; + + let cell = noun + .in_space(space) + .as_cell() + .map_err(|_| NounDecodeError::ExpectedCell)?; + let lmp = LockMerkleProof::from_noun(&cell.head().noun(), space)?; + + let rest = cell + .tail() + .as_cell() + .map_err(|_| NounDecodeError::ExpectedCell)?; + let pkh = PkhSignature::from_noun(&rest.head().noun(), space)?; + + let rest = rest + .tail() + .as_cell() + .map_err(|_| NounDecodeError::ExpectedCell)?; + let hax = ZMap::::from_noun(&rest.head().noun(), space)?; + + let tim = rest + .tail() + .as_atom() + .map_err(|_| NounDecodeError::ExpectedAtom)? + .as_u64() + .map_err(|_| NounDecodeError::Custom("witness tim must fit in u64".into()))?; + + // Upstream `Witness::from_noun` hardcodes `tim: 0` and Hoon requires + // `~`; a non-zero value is not a valid witness encoding. + if tim != 0 { + return Err(NounDecodeError::Custom("witness tim must be 0 (~)".into())); + } + + Ok(Self { lmp, pkh, hax, tim }) + } +} + impl Witness { /// Compute hashable for witness /// @@ -871,7 +922,7 @@ impl Witness { /// leaf+tim.form /// == /// ``` - pub fn to_hashable(&self) -> Hashable { + pub fn to_hashable(&self) -> Result { use crate::hashing::hasher::hash_hashable; // Hash the lock merkle proof @@ -881,15 +932,15 @@ impl Witness { let pkh_hash = self.pkh.to_hash(); // Hash the hax map - let hax_hashable = self.hashable_hax(); - let hax_hash = hash_hashable(&hax_hashable); + let hax_hashable = self.hashable_hax()?; + let hax_hash = hash_hashable(&hax_hashable)?; - Hashable::cell_chain([ + Ok(Hashable::cell_chain([ Hashable::Hash(lmp_hash), Hashable::Hash(pkh_hash), Hashable::Hash(hax_hash), Hashable::leaf_from_atom(&self.tim.to_le_bytes()), - ]) + ])) } /// Compute hashable for hax map @@ -904,9 +955,9 @@ impl Witness { /// $(m l.m) /// $(m r.m) /// ``` - fn hashable_hax(&self) -> Hashable { - self.hax.to_hashable( - |hash| Hashable::Hash(hash.clone()), + fn hashable_hax(&self) -> Result { + self.hax.try_to_hashable( + |hash| Ok(Hashable::Hash(hash.clone())), |untyped_noun| { // hashable-noun recursively builds cells for cells, leaves for atoms // But since UntypedNoun is just jammed bytes, we treat it as a leaf @@ -915,14 +966,15 @@ impl Witness { ) } - pub fn to_hash(&self) -> Hash { + pub fn to_hash(&self) -> Result { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()?) } } fn decode_hoon_list( noun: &Noun, + space: &NounSpace, context: &'static str, mut decode_item: F, ) -> Result, noun_serde::NounDecodeError> @@ -933,13 +985,14 @@ where let mut current = *noun; loop { - if let Ok(cell) = current.as_cell() { - items.push(decode_item(&cell.head())?); - current = cell.tail(); + let handle = current.in_space(space); + if let Ok(cell) = handle.as_cell() { + items.push(decode_item(&cell.head().noun())?); + current = cell.tail().noun(); continue; } - let atom = current + let atom = handle .as_atom() .map_err(|_| noun_serde::NounDecodeError::ExpectedAtom)?; match atom.as_u64()? { @@ -1035,7 +1088,8 @@ impl LockMerkleProof { pub fn to_hashable(&self) -> Hashable { use crate::hashing::hasher::hash_hashable; - let spend_condition_hash = hash_hashable(&self.spend_condition().to_hashable()); + let spend_condition_hash = hash_hashable(&self.spend_condition().to_hashable()) + .expect("spend-condition hashable is u64-based"); match self { Self::Full(proof) => Hashable::cell_chain([ @@ -1054,13 +1108,13 @@ impl LockMerkleProof { pub fn to_hash(&self) -> Hash { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("lock-merkle-proof hashable is u64-based") } } impl NounDecode for LockMerkleProof { - fn from_noun(noun: &Noun) -> Result { - if let Ok(full) = LockMerkleProofFull::from_noun(noun) { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { + if let Ok(full) = LockMerkleProofFull::from_noun(noun, space) { if full.version != nockvm_macros::tas!(b"full") { return Err(noun_serde::NounDecodeError::Custom( "lock-merkle-proof version must be %full".into(), @@ -1069,7 +1123,7 @@ impl NounDecode for LockMerkleProof { return Ok(Self::Full(full)); } - Ok(Self::Stub(LockMerkleProofStub::from_noun(noun)?)) + Ok(Self::Stub(LockMerkleProofStub::from_noun(noun, space)?)) } } @@ -1134,14 +1188,16 @@ impl SpendCondition { pub fn to_hash(&self) -> Hash { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("spend-condition hashable is u64-based") } } impl NounDecode for SpendCondition { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { Ok(Self { - p: decode_hoon_list(noun, "spend-condition", LockPrimitive::from_noun)?, + p: decode_hoon_list(noun, space, "spend-condition", |item| { + LockPrimitive::from_noun(item, space) + })?, }) } } @@ -1257,37 +1313,37 @@ pub struct LockPrimitive { pub body: LockPrimitiveBody, } -use nockapp::AtomExt; - impl NounDecode for LockPrimitive { - fn from_noun(noun: &nockvm::noun::Noun) -> Result { - let head = noun + fn from_noun( + noun: &nockvm::noun::Noun, + space: &nockvm::noun::NounSpace, + ) -> Result { + let cell = noun + .in_space(space) .as_cell() - .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)? + .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; + let head = cell .head() .as_atom() .map_err(|_| noun_serde::NounDecodeError::ExpectedAtom)? .into_string()?; - let body = noun - .as_cell() - .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)? - .tail(); + let body = cell.tail().noun(); match head.as_str() { "pkh" => Ok(LockPrimitive { header: head, - body: LockPrimitiveBody::Pkh(Pkh::from_noun(&body)?), + body: LockPrimitiveBody::Pkh(Pkh::from_noun(&body, space)?), }), "tim" => Ok(LockPrimitive { header: head, - body: LockPrimitiveBody::Tim(Tim::from_noun(&body)?), + body: LockPrimitiveBody::Tim(Tim::from_noun(&body, space)?), }), "hax" => Ok(LockPrimitive { header: head, - body: LockPrimitiveBody::Hax(Hax::from_noun(&body)?), + body: LockPrimitiveBody::Hax(Hax::from_noun(&body, space)?), }), "brn" => Ok(LockPrimitive { header: head, - body: LockPrimitiveBody::Brn(Brn::from_noun(&body)?), + body: LockPrimitiveBody::Brn(Brn::from_noun(&body, space)?), }), _ => Err(noun_serde::NounDecodeError::InvalidEnumVariant), } @@ -1371,17 +1427,20 @@ impl MerkleProof { pub fn to_hash(&self) -> Hash { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("merkle-proof hashable is u64-based") } } impl NounDecode for MerkleProof { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { let cell = noun + .in_space(space) .as_cell() .map_err(|_| noun_serde::NounDecodeError::ExpectedCell)?; - let root = Hash::from_noun(&cell.head())?; - let path = decode_hoon_list(&cell.tail(), "merkle proof path", Hash::from_noun)?; + let root = Hash::from_noun(&cell.head().noun(), space)?; + let path = decode_hoon_list(&cell.tail().noun(), space, "merkle proof path", |item| { + Hash::from_noun(item, space) + })?; Ok(Self { root, path }) } } @@ -1429,7 +1488,7 @@ impl PkhSignature { pub fn to_hash(&self) -> Hash { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("pkh-signature hashable is u64-based") } } @@ -1456,7 +1515,7 @@ impl PkhSignatureValue { pub fn to_hash(&self) -> Hash { use crate::hashing::hasher::hash_hashable; - hash_hashable(&self.to_hashable()) + hash_hashable(&self.to_hashable()).expect("pkh-signature-value hashable is u64-based") } } @@ -1504,7 +1563,9 @@ mod tests { &[version_noun, spend_condition_noun, D(7), merkle_proof_noun], ); - let decoded = LockMerkleProof::from_noun(&full_lmp).expect("full lmp should decode"); + let space = slab.noun_space(); + let decoded = + LockMerkleProof::from_noun(&full_lmp, &space).expect("full lmp should decode"); match decoded { LockMerkleProof::Full(full) => { @@ -1529,7 +1590,9 @@ mod tests { let mut slab: NounSlab = NounSlab::new(); let encoded = full.to_noun(&mut slab); - let decoded = LockMerkleProof::from_noun(&encoded).expect("full lmp should round-trip"); + let space = slab.noun_space(); + let decoded = + LockMerkleProof::from_noun(&encoded, &space).expect("full lmp should round-trip"); match decoded { LockMerkleProof::Full(full) => { @@ -1550,7 +1613,9 @@ mod tests { let mut slab: NounSlab = NounSlab::new(); let encoded = stub.to_noun(&mut slab); - let decoded = LockMerkleProof::from_noun(&encoded).expect("stub lmp should round-trip"); + let space = slab.noun_space(); + let decoded = + LockMerkleProof::from_noun(&encoded, &space).expect("stub lmp should round-trip"); match decoded { LockMerkleProof::Stub(stub) => { @@ -1571,7 +1636,8 @@ mod tests { let primitive_noun = primitive.to_noun(&mut slab); let malformed_list = T(&mut slab, &[primitive_noun, D(1)]); - let err = SpendCondition::from_noun(&malformed_list) + let space = slab.noun_space(); + let err = SpendCondition::from_noun(&malformed_list, &space) .expect_err("spend-condition decode should reject improper list"); assert!(err.to_string().contains("spend-condition must be a list")); @@ -1586,9 +1652,114 @@ mod tests { .to_noun(&mut slab); let malformed = T(&mut slab, &[hash_noun, D(1)]); - let err = - MerkleProof::from_noun(&malformed).expect_err("merkle proof path should reject atom"); + let space = slab.noun_space(); + let err = MerkleProof::from_noun(&malformed, &space) + .expect_err("merkle proof path should reject atom"); assert!(err.to_string().contains("merkle proof path must be a list")); } } +#[cfg(test)] +mod hardening_tests { + use super::*; + use nockapp::AtomExt; + use nockvm::noun::{D, T}; + + fn unbased_note_data() -> NoteData { + let mut slab: NounSlab = NounSlab::new(); + // A 72-bit atom: legal in a jam, but not belt-based. + let bytes = Bytes::copy_from_slice(&[1u8; 9]); + let atom = nockvm::noun::Atom::from_bytes(&mut slab, &bytes).as_noun(); + slab.set_root(atom); + + let mut map = ZMap::new(); + map.put("big".to_string(), UntypedNoun { p: slab.jam() }); + NoteData { map } + } + + #[test] + fn note_data_with_unbased_atom_returns_error() { + let note_data = unbased_note_data(); + let err = note_data + .to_hash() + .expect_err("unbased note-data must not fabricate a digest"); + assert!( + matches!(err, Tip5Error::HashingError(_)), + "unexpected error: {err:?}" + ); + } + + #[test] + fn spends_with_unbased_note_data_return_error() { + let mut spends = ZMap::new(); + let spend = Spend { + version: 1, + body: SpendBody::V1(SpendV1 { + witness: Witness { + lmp: LockMerkleProof::new_stub( + SpendCondition { p: Vec::new() }, + 1, + MerkleProof { + root: Hash { values: [0; 5] }, + path: Vec::new(), + }, + ), + pkh: PkhSignature { map: ZMap::new() }, + hax: ZMap::new(), + tim: 0, + }, + seeds: SeedsV1 { + set: { + let mut set = ZSet::new(); + set.put(SeedV1 { + output_source: None, + lock_root: Hash { values: [1; 5] }, + note_data: unbased_note_data(), + gift: Coins { value: 1 }, + parent_hash: Hash { values: [2; 5] }, + }); + set + }, + }, + fee: Coins { value: 1 }, + }), + }; + let name = NName { + p: vec![Hash { values: [3; 5] }, Hash { values: [4; 5] }], + }; + spends.put(name, spend); + + let result = compute_tx_id_v1(&SpendsV1 { map: spends }); + assert!( + result.is_err(), + "unbased note-data must error, got {result:?}" + ); + } + + #[test] + fn witness_rejects_nonzero_tim() { + let mut slab: NounSlab = NounSlab::new(); + let witness = Witness { + lmp: LockMerkleProof::new_stub( + SpendCondition { p: Vec::new() }, + 1, + MerkleProof { + root: Hash { values: [0; 5] }, + path: Vec::new(), + }, + ), + pkh: PkhSignature { map: ZMap::new() }, + hax: ZMap::new(), + tim: 0, + }; + + let lmp = witness.lmp.to_noun(&mut slab); + let pkh = witness.pkh.to_noun(&mut slab); + let hax = witness.hax.to_noun(&mut slab); + let bad = T(&mut slab, &[lmp, pkh, hax, D(1)]); + + let space = slab.noun_space(); + let err = Witness::from_noun(&bad, &space).expect_err("non-zero tim must be rejected"); + assert!(err.to_string().contains("tim must be 0")); + } +} diff --git a/tx-types/src/tx_builder_batch.rs b/tx-types/src/tx_builder_batch.rs index d53c0f1..4ffaf51 100644 --- a/tx-types/src/tx_builder_batch.rs +++ b/tx-types/src/tx_builder_batch.rs @@ -524,7 +524,8 @@ fn create_batch_spends_1( // === Build lock merkle proof === // Build a merkle proof showing that the spend-condition is in the lock tree - let lmp = build_lock_merkle_proof(input_lock.clone(), 1); + let lmp = + build_lock_merkle_proof(input_lock.clone(), 1, crate::fee_calculator::BYTHOS_PHASE); // Create witness with proper lock merkle proof let witness = Witness { @@ -564,12 +565,12 @@ fn create_batch_spends_1( fn lock_hash(spend_condition: &SpendCondition) -> Hash { let hashable = spend_condition.to_hashable(); - hash_hashable(&hashable) + hash_hashable(&hashable).expect("spend-condition hashable is u64-based") } fn note_hash_v0(note: &NNoteV0) -> Hash { let hashable = note.to_hashable(); - hash_hashable(&hashable) + hash_hashable(&hashable).expect("v0 note hashable is u64-based") } fn note_hash_v1(note: &NNoteV1) -> Hash { @@ -793,7 +794,8 @@ fn create_unsigned_batch_spends_1( // === Build lock merkle proof === // Build a merkle proof showing that the spend-condition is in the lock tree - let lmp = build_lock_merkle_proof(input_lock.clone(), 1); + let lmp = + build_lock_merkle_proof(input_lock.clone(), 1, crate::fee_calculator::BYTHOS_PHASE); // Create witness with proper lock merkle proof let witness = Witness { diff --git a/tx-types/src/tx_builder_v1.rs b/tx-types/src/tx_builder_v1.rs index f0f4723..a1d1e4c 100644 --- a/tx-types/src/tx_builder_v1.rs +++ b/tx-types/src/tx_builder_v1.rs @@ -26,12 +26,13 @@ use crate::collections::{ZMap, ZSet}; use crate::generic_noun::UntypedNoun; use crate::hashing::hashable::Hashable; use crate::hashing::hasher::hash_hashable; +use crate::hashing::tip5::Tip5Error; use crate::signer::schnorr_sign_digest; use crate::transaction_types::*; use crate::transaction_types_v0::*; use crate::transaction_types_v1::*; use nockapp::noun::slab::NounSlab; -use nockvm::noun::{Noun, D, T}; +use nockvm::noun::{Noun, NounAllocator, NounSpace, D, T}; use noun_serde::{NounDecode, NounDecodeError, NounEncode}; /// Order structure representing a payment to a recipient @@ -52,10 +53,10 @@ pub enum LockData { } impl NounDecode for LockData { - fn from_noun(noun: &Noun) -> Result { + fn from_noun(noun: &Noun, space: &NounSpace) -> Result { // lock-data is a cell: [version lock:transact] // For V0: [%0 =lock:transact] where lock is a spend-condition - let cell = match noun.as_cell() { + let cell = match noun.in_space(space).as_cell() { Ok(c) => c, Err(_) => return Err(NounDecodeError::ExpectedCell), }; @@ -90,7 +91,7 @@ impl NounDecode for LockData { } // Decode the spend-condition - let spend_condition = SpendCondition::from_noun(&lock_noun)?; + let spend_condition = SpendCondition::from_noun(&lock_noun.noun(), space)?; Ok(LockData::V0(spend_condition)) } @@ -125,7 +126,8 @@ pub fn lock_data_to_untyped_noun(lock_data: &LockData) -> UntypedNoun { let lock_data_noun = T(&mut slab, &[version_tag, lock_noun]); // Copy the noun into the slab and jam it - slab.copy_into(lock_data_noun); + let space = slab.noun_space(); + slab.copy_into(lock_data_noun, &space); let jammed = slab.jam(); UntypedNoun { p: jammed } @@ -474,7 +476,7 @@ pub fn create_spends_0( // Add gift seed (lines 99-106) if gift_portion > 0 { - let lock_root = hash_hashable(&output_lock.to_hashable()); + let lock_root = hash_hashable(&output_lock.to_hashable()).map_err(|e| e.to_string())?; let parent_hash = note.to_hash(); let gift_seed = SeedV1 { @@ -787,7 +789,7 @@ pub fn create_spends_1( // =? seeds (gth gift-portion 0) if gift_portion > 0 { // Compute lock root hash for the output lock - let lock_root = hash_hashable(&output_lock.to_hashable()); + let lock_root = hash_hashable(&output_lock.to_hashable()).map_err(|e| e.to_string())?; // Get parent hash (hash of the note being spent) let parent_hash = note.name.to_hash(); @@ -828,7 +830,8 @@ pub fn create_spends_1( // Build a merkle proof showing that the spend-condition is in the lock tree // =/ lmp=lock-merkle-proof:transact // (build-lock-merkle-proof:lock:transact p.input-lock 1) - let lmp = build_lock_merkle_proof(input_lock.clone(), 1); + let lmp = + build_lock_merkle_proof(input_lock.clone(), 1, crate::fee_calculator::BYTHOS_PHASE); // === Create spend and witness === (lines 208-215) // Build the Spend1 structure with witness @@ -941,7 +944,7 @@ fn create_refund( let note_data = NoteData { map: note_data_map }; // Calculate lock root hash - let lock_root = hash_hashable(&lok.to_hashable()); + let lock_root = hash_hashable(&lok.to_hashable()).expect("refund lock hashable is u64-based"); // Get parent hash let parent_hash = note.name.to_hash(); @@ -990,7 +993,7 @@ pub fn sign_spend_v1( ) -> Result { // Calculate the signature hash for the spend // In Hoon: (sig-hash:spend:v1 sp) - let sig_hash = compute_spend_v1_sig_hash(&spend_body); + let sig_hash = compute_spend_v1_sig_hash(&spend_body).map_err(|e| e.to_string())?; // Sign the hash using Schnorr signature let (chal_t8, sig_t8) = schnorr_sign_digest(sign_key, pubkey.clone(), sig_hash); @@ -1036,11 +1039,11 @@ pub fn sign_spend_v1( /// %- hash-hashable:tip5 /// [(sig-hashable:seeds seeds.sen) leaf+fee.sen] /// ``` -fn compute_spend_v1_sig_hash(spend: &SpendV1) -> Hash { +fn compute_spend_v1_sig_hash(spend: &SpendV1) -> Result { use crate::hashing::hasher::hash_hashable; // Get the sig-hashable for seeds - let seeds_hashable = spend.seeds.to_sig_hashable(); + let seeds_hashable = spend.seeds.to_sig_hashable()?; // Create the fee leaf let fee_hashable = spend.fee.to_hashable(); @@ -1108,7 +1111,7 @@ fn create_refund_v0_to_v1( let note_data = NoteData { map: note_data_map }; // Calculate lock root hash - let lock_root = hash_hashable(&lok.to_hashable()); + let lock_root = hash_hashable(&lok.to_hashable()).expect("refund lock hashable is u64-based"); // Get parent hash let parent_hash = note.to_hash(); @@ -1152,7 +1155,7 @@ pub fn sign_spend_v0_to_v1( pubkey: SchnorrPubkey, ) -> Result { // Calculate the signature hash using the SpendV0ToV1's method - let sig_hash = spend_body.compute_sig_hash(); + let sig_hash = spend_body.compute_sig_hash().map_err(|e| e.to_string())?; // Sign the hash using Schnorr signature let (chal_t8, sig_t8) = schnorr_sign_digest(sign_key, pubkey.clone(), sig_hash); diff --git a/tx-types/src/tx_to_noun.rs b/tx-types/src/tx_to_noun.rs index 65b9b20..a3a27f4 100644 --- a/tx-types/src/tx_to_noun.rs +++ b/tx-types/src/tx_to_noun.rs @@ -1736,7 +1736,7 @@ mod tests { let inputs = Inputs { p: inputs_zmap }; // Hash the inputs using the new to_hash() method - let computed_hash = inputs.to_hash(); + let computed_hash = inputs.to_hash().expect("v0 inputs hashable is u64-based"); // Expected hash from Hoon let expected_hash = Hash { diff --git a/tx-types/src/validation/signature_verification.rs b/tx-types/src/validation/signature_verification.rs index c0ffd3b..e22a7d2 100644 --- a/tx-types/src/validation/signature_verification.rs +++ b/tx-types/src/validation/signature_verification.rs @@ -44,12 +44,31 @@ pub fn schnorr_verify_digest( message: Hash, signature: SchnorrSignature, ) -> bool { + // 0. Reject non-canonical T8 encodings. The scalar is reconstructed as + // base-2^32 digits, so every limb must be < 2^32 (upstream + // `is_canonical`, Hoon `(levy ... (lth n b-32))`). Without this check the + // same scalar has malleable encodings. + const LIMB_BOUND: u64 = 1 << 32; + if signature + .chal + .values + .values + .iter() + .chain(signature.sig.values.values.iter()) + .any(|&limb| limb >= LIMB_BOUND) + { + return false; + } + // 1. Convert signature components to big-endian format let chal_be = t8_to_be32(&signature.chal.values); let sig_be = t8_to_be32(&signature.sig.values); - // 2. Validate that both components are < curve order - // This prevents malleability attacks and ensures valid scalars + // 2. Validate that both components are non-zero and < curve order + // This mirrors Hoon `(gth chal 0) (gth sig 0)` and prevents malleability. + if be32_is_zero(&chal_be) || be32_is_zero(&sig_be) { + return false; + } if !be32_lt(&chal_be, &CHEETAH_N) || !be32_lt(&sig_be, &CHEETAH_N) { return false; } @@ -92,6 +111,10 @@ pub fn schnorr_verify_digest( chal_be == expected_chal_be } +fn be32_is_zero(value: &[u8; 32]) -> bool { + value.iter().all(|&byte| byte == 0) +} + #[cfg(test)] mod tests { use super::*; @@ -363,4 +386,67 @@ mod tests { println!("✓ Wrong public key correctly fails verification"); } + + fn dummy_pubkey() -> SchnorrPubkey { + SchnorrPubkey { + x: F6LT { values: [0; 6] }, + y: F6LT { values: [0; 6] }, + inf: false, + } + } + + fn signature_with(chal: [u64; 8], sig: [u64; 8]) -> SchnorrSignature { + use crate::transaction_types::{Chal, Sig}; + SchnorrSignature { + chal: Chal { + values: T8 { values: chal }, + }, + sig: Sig { + values: T8 { values: sig }, + }, + } + } + + #[test] + fn test_rejects_zero_challenge() { + let signature = signature_with([0; 8], [1, 0, 0, 0, 0, 0, 0, 0]); + assert!(!schnorr_verify_digest( + dummy_pubkey(), + Hash { values: [0; 5] }, + signature + )); + } + + #[test] + fn test_rejects_zero_signature() { + let signature = signature_with([1, 0, 0, 0, 0, 0, 0, 0], [0; 8]); + assert!(!schnorr_verify_digest( + dummy_pubkey(), + Hash { values: [0; 5] }, + signature + )); + } + + #[test] + fn test_rejects_non_canonical_limbs() { + // A limb >= 2^32 is not a canonical base-2^32 digit. The same scalar + // could otherwise be encoded many ways (malleability). + let mut chal = [1, 0, 0, 0, 0, 0, 0, 0]; + chal[7] = 1 << 32; + let signature = signature_with(chal, [1, 0, 0, 0, 0, 0, 0, 0]); + assert!(!schnorr_verify_digest( + dummy_pubkey(), + Hash { values: [0; 5] }, + signature + )); + + let mut sig = [1, 0, 0, 0, 0, 0, 0, 0]; + sig[0] = 1 << 32; + let signature = signature_with([1, 0, 0, 0, 0, 0, 0, 0], sig); + assert!(!schnorr_verify_digest( + dummy_pubkey(), + Hash { values: [0; 5] }, + signature + )); + } } diff --git a/tx-types/src/validation/validator.rs b/tx-types/src/validation/validator.rs index bc60ff1..71e25c0 100644 --- a/tx-types/src/validation/validator.rs +++ b/tx-types/src/validation/validator.rs @@ -32,6 +32,15 @@ impl TransactionValidator { pub fn validate_transaction( transaction: &RawTransaction, ) -> Result<(), TransactionValidationError> { + // The checks below are V0-shaped. Returning Ok(()) for V1 would claim + // a validation that never happened, so fail closed until full V1 + // validation (sig-hash, m-of-n, hax preimages, Bythos LMP gate) lands. + if let RawTransaction::V1(_) = transaction { + return Err(TransactionValidationError::UnsupportedVersion( + "V1 transaction validation is not implemented".to_string(), + )); + } + // 1. Validate all signatures Self::validate_signatures(transaction)?; @@ -291,6 +300,8 @@ pub enum TransactionValidationError { InvalidTimelock(String), /// Invalid fee amount InvalidFee(String), + /// The transaction version cannot be validated by this validator + UnsupportedVersion(String), /// General validation error ValidationError(String), } @@ -308,6 +319,9 @@ impl std::fmt::Display for TransactionValidationError { write!(f, "Invalid timelock: {}", msg) } TransactionValidationError::InvalidFee(msg) => write!(f, "Invalid fee: {}", msg), + TransactionValidationError::UnsupportedVersion(msg) => { + write!(f, "Unsupported transaction version: {}", msg) + } TransactionValidationError::ValidationError(msg) => { write!(f, "Validation error: {}", msg) } @@ -317,6 +331,28 @@ impl std::fmt::Display for TransactionValidationError { impl std::error::Error for TransactionValidationError {} +#[cfg(test)] +mod version_tests { + use super::*; + use crate::collections::ZMap; + use crate::transaction_types::Hash; + use crate::transaction_types_v1::{RawTransactionV1, SpendsV1}; + + #[test] + fn validate_transaction_rejects_v1_as_unsupported() { + let tx = RawTransaction::V1(RawTransactionV1 { + version: 1, + id: Hash { values: [0; 5] }, + spends: SpendsV1 { map: ZMap::new() }, + }); + + match TransactionValidator::validate_transaction(&tx) { + Err(TransactionValidationError::UnsupportedVersion(_)) => {} + other => panic!("expected UnsupportedVersion, got {other:?}"), + } + } +} + #[cfg(all(test, feature = "legacy-tests"))] mod tests { use super::*; diff --git a/tx-types/tests/malloc_stack_release.rs b/tx-types/tests/malloc_stack_release.rs deleted file mode 100644 index 433bfe1..0000000 --- a/tx-types/tests/malloc_stack_release.rs +++ /dev/null @@ -1,43 +0,0 @@ -#![cfg(feature = "std")] - -use nockvm::mem::Memory; -use std::alloc::{GlobalAlloc, Layout, System}; -use std::sync::atomic::{AtomicUsize, Ordering}; - -const STACK_WORDS: usize = 1 << 20; -const STACK_BYTES: usize = STACK_WORDS * 8; -static LIVE_STACKS: AtomicUsize = AtomicUsize::new(0); -struct TrackingAllocator; - -unsafe impl GlobalAlloc for TrackingAllocator { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let ptr = unsafe { System.alloc(layout) }; - if !ptr.is_null() && layout.size() == STACK_BYTES { - LIVE_STACKS.fetch_add(1, Ordering::SeqCst); - } - ptr - } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - if layout.size() == STACK_BYTES { - LIVE_STACKS.fetch_sub(1, Ordering::SeqCst); - } - unsafe { System.dealloc(ptr, layout) }; - } -} - -#[global_allocator] -static ALLOCATOR: TrackingAllocator = TrackingAllocator; - -#[test] -fn temporary_malloc_stacks_release_their_owned_allocation() { - let baseline = LIVE_STACKS.load(Ordering::SeqCst); - for _ in 0..32 { - let layout = Layout::from_size_align(STACK_BYTES, align_of::()).unwrap(); - let ptr = unsafe { std::alloc::alloc(layout) }; - assert!(!ptr.is_null()); - let memory = Memory::Malloc(ptr, STACK_WORDS); - assert_eq!(LIVE_STACKS.load(Ordering::SeqCst), baseline + 1); - drop(memory); - assert_eq!(LIVE_STACKS.load(Ordering::SeqCst), baseline); - } -} diff --git a/use-git-nockchain.sh b/use-git-nockchain.sh deleted file mode 100755 index 51ab8f0..0000000 --- a/use-git-nockchain.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Script to switch back to git nockchain dependencies (default) - -set -e - -CARGO_TOML="Cargo.toml" - -if [ ! -f "$CARGO_TOML" ]; then - echo "Error: $CARGO_TOML not found in current directory" - exit 1 -fi - -if [ -f "$CARGO_TOML.bak" ]; then - echo "Restoring git dependencies..." - mv "$CARGO_TOML.bak" "$CARGO_TOML" - echo "Git dependencies restored. Run 'cargo clean' to ensure a fresh build." -else - echo "No backup found. Commenting out [patch] section..." - # Comment out the [patch."https://github.com/SWPSCO/nockchain.git"] section - sed -i.bak '/^\[patch\."https:\/\/github\.com\/SWPSCO\/nockchain\.git"\]/,/^ibig = { path/s/^\([^#]\)/# \1/' "$CARGO_TOML" - rm "$CARGO_TOML.bak" - echo "Git dependencies enabled. Run 'cargo clean' to ensure a fresh build." -fi diff --git a/use-local-nockchain.sh b/use-local-nockchain.sh deleted file mode 100755 index 738265c..0000000 --- a/use-local-nockchain.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# Script to enable local nockchain dependencies for WASM compilation - -set -e - -CARGO_TOML="Cargo.toml" - -if [ ! -f "$CARGO_TOML" ]; then - echo "Error: $CARGO_TOML not found in current directory" - exit 1 -fi - -echo "Enabling local nockchain dependencies..." - -# Uncomment the [patch."https://github.com/SWPSCO/nockchain.git"] section -sed -i.bak '/^# \[patch\."https:\/\/github\.com\/SWPSCO\/nockchain\.git"\]/,/^# ibig = { path/s/^# //' "$CARGO_TOML" - -echo "Local dependencies enabled. Run 'cargo clean' to ensure a fresh build." -echo "To revert, run: ./use-git-nockchain.sh" diff --git a/vanity/Cargo.toml b/vanity/Cargo.toml index c2cc23d..beb2f25 100644 --- a/vanity/Cargo.toml +++ b/vanity/Cargo.toml @@ -22,6 +22,6 @@ ibig = { workspace = true, default-features = false } [features] default = [] -std = ["tx-types/std", "dep:tempfile"] +std = ["tx-types/std", "tx-types/embedded", "dep:tempfile"] mnemonic = ["dep:bip39"] export = ["mnemonic", "dep:bs58", "dep:serde_json"]