From f3e7122d4351f941d1a9c7a43362b0e2b47a8840 Mon Sep 17 00:00:00 2001 From: Uncle Stinky Date: Fri, 9 Oct 2026 14:56:43 +0300 Subject: [PATCH] remove Header struct and implement stateless merkle verification Signed-off-by: Uncle Stinky --- pallets/helpers/Cargo.toml | 2 +- pallets/helpers/src/utxo.rs | 183 ++++++++++++++++++++---------------- pallets/weaver/Cargo.toml | 2 +- pallets/weaver/src/types.rs | 14 ++- 4 files changed, 109 insertions(+), 92 deletions(-) diff --git a/pallets/helpers/Cargo.toml b/pallets/helpers/Cargo.toml index 61103e1..81b39e0 100644 --- a/pallets/helpers/Cargo.toml +++ b/pallets/helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-helpers" -version = "0.0.12" +version = "0.0.13" description = "Cryptographic utility suite for custom runtimes: optimized Bitmaps, UTXO parsing, Merkle Tree proofs, and Hash Chain components." license.workspace = true authors.workspace = true diff --git a/pallets/helpers/src/utxo.rs b/pallets/helpers/src/utxo.rs index f0fb40a..ea1944d 100644 --- a/pallets/helpers/src/utxo.rs +++ b/pallets/helpers/src/utxo.rs @@ -3,6 +3,8 @@ use sp_std::vec::Vec; const MAX_INPUTS: usize = 3; const MAX_OUTPUTS: usize = 4; +const MAX_FLAGS_LEN: usize = 8; +const MAX_HASHES_LEN: usize = 16; const MAX_SCRIPT_LEN: usize = 512; pub const MAX_TX_LEN: usize = 4096; @@ -27,20 +29,17 @@ pub struct Transaction { lock_time: u32, } -pub struct Header { - pub merkle_root: H::Hash, - pub tx_count: u32, - pub tree_hashes: Vec, - pub flags: Vec, -} +pub struct Header(sp_std::marker::PhantomData); impl Header { - fn calc_tree_width(&self, h: u32) -> u32 { - (self.tx_count + (1 << h) - 1) >> h + fn calc_tree_width(tx_count: u32, h: u32) -> u32 { + (tx_count + (1 << h) - 1) >> h } fn traverse( - &self, + tx_count: u32, + flags: &[u8], + tree_hashes: &[H::Hash], height: u32, pos: u32, bits_used: &mut u32, @@ -51,19 +50,19 @@ impl Header { let byte_idx = (*bits_used / 8) as usize; let bit_idx = (*bits_used % 8) as u8; - if byte_idx >= self.flags.len() { + if byte_idx >= flags.len() { return Err(ParseError::InvalidVarint(VarintError::BufferTooShort)); } - let parent_of_match = ((self.flags[byte_idx] >> bit_idx) & 1) == 1; + let parent_of_match = ((flags[byte_idx] >> bit_idx) & 1) == 1; *bits_used += 1; if height == 0 || !parent_of_match { - if *hash_used as usize >= self.tree_hashes.len() { + if *hash_used as usize >= tree_hashes.len() { return Err(ParseError::InvalidLength); } - let curr_hash = self.tree_hashes[*hash_used as usize]; + let curr_hash = tree_hashes[*hash_used as usize]; *hash_used += 1; if height == 0 && parent_of_match && curr_hash == target_txid { @@ -73,7 +72,10 @@ impl Header { return Ok(curr_hash); } - let left = self.traverse( + let left = Self::traverse( + tx_count, + flags, + tree_hashes, height - 1, pos * 2, bits_used, @@ -82,8 +84,11 @@ impl Header { target_txid, )?; - let right = if pos * 2 + 1 < self.calc_tree_width(height - 1) { - let right = self.traverse( + let right = if pos * 2 + 1 < Self::calc_tree_width(tx_count, height - 1) { + let right = Self::traverse( + tx_count, + flags, + tree_hashes, height - 1, pos * 2 + 1, bits_used, @@ -109,17 +114,83 @@ impl Header { Ok(current_node_hash) } - pub fn extract_proof(&self, target_txid: H::Hash) -> Result { + pub fn verify_utxo_merkle_proof( + txproof_bytes: &[u8], + target_txid: H::Hash, + ) -> Result<(), ParseError> { + if txproof_bytes.len() < 85 { + return Err(ParseError::InvalidLength); + } + + let header_bytes = &txproof_bytes[..80]; + let mut bytes = &txproof_bytes[80..]; + + let mut merkle_root = [0u8; 32]; + merkle_root.copy_from_slice(&header_bytes[36..68]); + + if bytes.len() < 4 { + return Err(ParseError::InvalidLength); + } + + let tx_count = u32::from_le_bytes( + bytes[..4] + .try_into() + .map_err(|_| ParseError::TypeConversionFailed)?, + ); + bytes = &bytes[4..]; + let hash_count = read_varint(&mut bytes)?; + + if hash_count > MAX_HASHES_LEN { + return Err(ParseError::InvalidLength); + } + + let hash_count_bytes = hash_count + .checked_mul(32) + .ok_or(ParseError::TypeConversionFailed)?; + + if bytes.len() < hash_count_bytes { + return Err(ParseError::InvalidLength); + } + + let tree_hashes_bytes = &bytes[..hash_count_bytes]; + bytes = &bytes[hash_count_bytes..]; + + let tree_hashes: Vec = tree_hashes_bytes + .chunks_exact(32) + .map(|chunk| H::from_slice(chunk)) + .collect(); + + let flags_count = read_varint(&mut bytes)?; + + if flags_count > MAX_FLAGS_LEN { + return Err(ParseError::InvalidLength); + } + + if bytes.len() < flags_count { + return Err(ParseError::InvalidLength); + } + + let flags = bytes[..flags_count].to_vec(); + bytes = &bytes[flags_count..]; + + if !bytes.is_empty() { + return Err(ParseError::InvalidLength); + } + let mut bits_used = 0u32; let mut hash_used = 0u32; let mut matched_index = None; let mut max_height = 0; - while (1 << max_height) < self.tx_count { + + while (1 << max_height) < tx_count { max_height += 1; } - let computed_root = self.traverse( + let computed_root = Self::traverse( + tx_count, + &flags, + &tree_hashes, max_height, 0, &mut bits_used, @@ -128,19 +199,23 @@ impl Header { target_txid, )?; - if hash_used as usize != self.tree_hashes.len() { + if hash_used as usize != tree_hashes.len() { return Err(ParseError::InvalidLength); } - if (bits_used + 7) / 8 != self.flags.len() as u32 { + if (bits_used + 7) / 8 != flags.len() as u32 { return Err(ParseError::InvalidLength); } - if matched_index.is_some() { - Ok(computed_root) - } else { - Err(ParseError::TransactionNotFoundInProof) + if matched_index.is_none() { + return Err(ParseError::TransactionNotFoundInProof); } + + if computed_root != H::from_slice(&merkle_root) { + return Err(ParseError::InvalidMerkleRoot); + } + + Ok(()) } } @@ -401,6 +476,7 @@ fn read_varint(bytes: &mut &[u8]) -> Result { pub enum ParseError { InvalidLength, ExceededMaxLimits, + InvalidMerkleRoot, TypeConversionFailed, TransactionNotFoundInProof, InvalidVarint(VarintError), @@ -534,60 +610,3 @@ impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Transaction { }) } } - -impl<'a, H: GhostHasher> TryFrom<&'a [u8]> for Header { - type Error = ParseError; - - fn try_from(mut bytes: &[u8]) -> Result { - if bytes.len() < 85 { - return Err(ParseError::InvalidLength); - } - - let header_bytes = &bytes[..80]; - bytes = &bytes[80..]; - - let mut merkle_root = [0u8; 32]; - merkle_root.copy_from_slice(&header_bytes[36..68]); - - if bytes.len() < 4 { - return Err(ParseError::InvalidLength); - } - let tx_count = u32::from_le_bytes( - bytes[..4] - .try_into() - .map_err(|_| ParseError::TypeConversionFailed)?, - ); - bytes = &bytes[4..]; - - let hash_count = read_varint(&mut bytes)?; - if bytes.len() < hash_count * 32 { - return Err(ParseError::InvalidLength); - } - - let tree_hashes_bytes = &bytes[..hash_count * 32]; - bytes = &bytes[hash_count * 32..]; - - let tree_hashes: Vec = tree_hashes_bytes - .chunks_exact(32) - .map(|chunk| H::from_slice(chunk)) - .collect(); - - let flags_count = read_varint(&mut bytes)?; - if bytes.len() < flags_count { - return Err(ParseError::InvalidLength); - } - let flags = bytes[..flags_count].to_vec(); - bytes = &bytes[flags_count..]; - - if !bytes.is_empty() { - return Err(ParseError::InvalidLength); - } - - Ok(Self { - merkle_root: H::from_slice(&merkle_root), - tx_count, - tree_hashes, - flags, - }) - } -} diff --git a/pallets/weaver/Cargo.toml b/pallets/weaver/Cargo.toml index 3b72278..03124ec 100644 --- a/pallets/weaver/Cargo.toml +++ b/pallets/weaver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-weaver" -version = "0.0.15" +version = "0.0.16" description = "Weaving a secure cryptographic tapestry across different external chains." license.workspace = true authors.workspace = true diff --git a/pallets/weaver/src/types.rs b/pallets/weaver/src/types.rs index dab7402..f934c69 100644 --- a/pallets/weaver/src/types.rs +++ b/pallets/weaver/src/types.rs @@ -483,7 +483,7 @@ impl UtxoThreadProof { pub fn verify_thread_proof( &self, - root_hash: H256, + block_hash: H256, gatekeeper_ref: &[u8], ) -> Option<(AccountId32, Balance)> where @@ -498,7 +498,7 @@ impl UtxoThreadProof { let is_chain_valid = verify_hash_ancestry::( header_bytes, self.ancestry_headers.iter(), - root_hash, + block_hash, ); if !is_chain_valid { return None; } @@ -507,13 +507,11 @@ impl UtxoThreadProof { self.getrawtransaction_bytes.as_ref() ).ok()?; - let header = Header::::try_from( - self.gettxproof_bytes.as_ref(), - ).ok()?; - let txid_preimage = transaction.compute_txid()?; - let root_hash = header.extract_proof(txid_preimage).ok()?; - if root_hash != header.merkle_root { return None; } + Header::::verify_utxo_merkle_proof( + self.gettxproof_bytes.as_ref(), + txid_preimage, + ).ok()?; #[cfg(not(any(test, feature = "runtime-benchmarks")))] let destination_script = Transaction::::convert_into_p2tr(gatekeeper_ref)?;