From 6b0033425cf696a41259b881a58ab1b71619f9b8 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Thu, 1 Oct 2026 21:20:17 +0300 Subject: [PATCH 1/4] implement more secure entropy hedging for offchain RNG Signed-off-by: Uncle Stretch --- pallets/exodus/Cargo.toml | 2 +- pallets/exodus/src/benchmarking.rs | 3 +- pallets/exodus/src/lib.rs | 60 ++++++++++------------- pallets/exodus/src/types/local_storage.rs | 4 +- 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/pallets/exodus/Cargo.toml b/pallets/exodus/Cargo.toml index f84b526..a6ee67e 100644 --- a/pallets/exodus/Cargo.toml +++ b/pallets/exodus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-exodus" -version = "0.0.17" +version = "0.0.18" description = "Threshold signature generation with DKG included" license.workspace = true authors.workspace = true diff --git a/pallets/exodus/src/benchmarking.rs b/pallets/exodus/src/benchmarking.rs index 2da947c..fcf8949 100644 --- a/pallets/exodus/src/benchmarking.rs +++ b/pallets/exodus/src/benchmarking.rs @@ -85,8 +85,7 @@ mod benchmarks { dkg_state.insert_index(authority_index); QualificationDkgState::::insert(&network_curve, dkg_state); - let mut rng = Pallet::::get_offchain_rng(authority_index); - + let mut rng = ChaCha20Rng::seed_from_u64(694201337); let (_, public_package) = network_curve.dkg_part1( authority_index, max_signers, diff --git a/pallets/exodus/src/lib.rs b/pallets/exodus/src/lib.rs index 7f43c6b..e879e24 100644 --- a/pallets/exodus/src/lib.rs +++ b/pallets/exodus/src/lib.rs @@ -31,7 +31,7 @@ use sp_io::hashing::blake2_256; use ghost_helpers::{ get_byzantium_quorum_threshold, SubstrateBlake2Hasher, - hash_chain::sequential_hash, + hash_chain::{cumulative_hash, sequential_hash}, networks::{NetworkData, NetworkCurve, NetworkType}, bounded_bitmap::{validate_bitmap_sizes, BoundedBitmap}, }; @@ -3559,12 +3559,10 @@ impl Pallet { let authority_index = exodus_storage.authority_index; let network_curve = exodus_storage.network_curve; - #[cfg(not(any(test, feature = "runtime-benchmarks")))] - let mut rng = Self::get_offchain_rng(); - - #[cfg(any(test, feature = "runtime-benchmarks"))] - let mut rng = Self::get_offchain_rng(authority_index); + let exodus_storage_encoded = exodus_storage.encode(); + let rng_context = [exodus_storage_encoded.as_slice(), secret_share]; + let mut rng = Self::get_offchain_rng(&rng_context); let (nonce, hiding_commitment, binding_commitment) = network_curve .generate_nonce_commitment(secret_share, &mut rng)?; @@ -3776,12 +3774,15 @@ impl Pallet { )); } - #[cfg(not(any(test, feature = "runtime-benchmarks")))] - let mut rng = Self::get_offchain_rng(); + let dkg_storage_encoded = dkg_storage.encode(); + let qualification_state_encoded = qualification_state.encode(); - #[cfg(any(test, feature = "runtime-benchmarks"))] - let mut rng = Self::get_offchain_rng(authority_index); + let rng_context = [ + dkg_storage_encoded.as_slice(), + qualification_state_encoded.as_slice(), + ]; + let mut rng = Self::get_offchain_rng(&rng_context); let (secret_package, public_package) = network_curve .dkg_part1(authority_index, max_signers, min_signers, &mut rng)?; @@ -4736,11 +4737,17 @@ impl Pallet { dkg_index: DkgIndex, network_curve: NetworkCurve, ) { - #[cfg(not(any(test, feature = "runtime-benchmarks")))] - let mut rng = Self::get_offchain_rng(); + let authority_index_bytes = authority_index.to_le_bytes(); + let dkg_index_bytes = dkg_index.to_le_bytes(); + let network_curve_bytes = [network_curve as u8]; - #[cfg(any(test, feature = "runtime-benchmarks"))] - let mut rng = Self::get_offchain_rng(authority_index); + let rng_context = [ + round1_secret_package, + authority_index_bytes.as_slice(), + dkg_index_bytes.as_slice(), + network_curve_bytes.as_slice(), + ]; + let mut rng = Self::get_offchain_rng(&rng_context); for (&receiver_index, round2_package) in round2_packages.iter() { if receiver_index == authority_index { continue; } @@ -4811,26 +4818,13 @@ impl Pallet { } } - #[cfg(any(test, feature = "runtime-benchmarks"))] - fn get_offchain_rng(authority_index: AuthIndex) -> ChaCha20Rng { - let mut seed = [0u8; 32]; - - let unique_id = (authority_index as u64).saturating_add(1); - let index_bytes = unique_id.to_le_bytes(); - - for (i, &byte) in index_bytes.iter().enumerate() { - if i < seed.len() { - seed[i] ^= byte; - } - } - - rand_chacha::ChaCha20Rng::from_seed(seed) - } - - #[cfg(not(any(test, feature = "runtime-benchmarks")))] - fn get_offchain_rng() -> ChaCha20Rng { + fn get_offchain_rng(context: &[&[u8]]) -> ChaCha20Rng { let seed = sp_io::offchain::random_seed(); - ChaCha20Rng::from_seed(seed) + let seeded_context = sp_std::iter::once(&seed[..]) + .chain(context.iter().copied()); + + let hashed_seed = cumulative_hash::(seeded_context); + ChaCha20Rng::from_seed(hashed_seed.to_fixed_bytes()) } fn create_storage_key(first: &[u8], second: &[u8]) -> Vec { diff --git a/pallets/exodus/src/types/local_storage.rs b/pallets/exodus/src/types/local_storage.rs index 8bfce89..81a7c59 100644 --- a/pallets/exodus/src/types/local_storage.rs +++ b/pallets/exodus/src/types/local_storage.rs @@ -75,7 +75,7 @@ impl ExodusStorageWrapper { } } -#[derive(Default)] +#[derive(Default, Encode)] pub struct ExodusStorage where BlockNumber: Default, @@ -365,7 +365,7 @@ where } } -#[derive(Default, Clone, Copy)] +#[derive(Default, Encode, Clone, Copy)] pub struct DkgStorage where BlockNumber: Copy + Clone, From 8abd114a5b22991de60ccc9ab357289f2f5cd578 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Thu, 1 Oct 2026 21:35:38 +0300 Subject: [PATCH 2/4] more secure and optimized Round 5 metadata hashing pipeline Signed-off-by: Uncle Stretch --- pallets/exodus/Cargo.toml | 2 +- pallets/exodus/src/benchmarking.rs | 7 ++----- pallets/exodus/src/lib.rs | 11 ++++------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pallets/exodus/Cargo.toml b/pallets/exodus/Cargo.toml index a6ee67e..101c19f 100644 --- a/pallets/exodus/Cargo.toml +++ b/pallets/exodus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-exodus" -version = "0.0.18" +version = "0.0.19" description = "Threshold signature generation with DKG included" license.workspace = true authors.workspace = true diff --git a/pallets/exodus/src/benchmarking.rs b/pallets/exodus/src/benchmarking.rs index fcf8949..e9f1064 100644 --- a/pallets/exodus/src/benchmarking.rs +++ b/pallets/exodus/src/benchmarking.rs @@ -352,11 +352,8 @@ mod benchmarks { dummy_verifying_key, ).expect("Bounded verifying key should be correct"); - let metadata_hashes = [ - SubstrateBlake2Hasher::hash(dummy_merkle_root.as_ref()), - SubstrateBlake2Hasher::hash(bounded_verifying_key.as_ref()), - ]; - let verifying_hash = sequential_hash::(metadata_hashes); + let metadata_hashes = [dummy_merkle_root.as_ref(), bounded_verifying_key.as_ref()]; + let verifying_hash = cumulative_hash::(metadata_hashes); let verification_key = (network_curve, dkg_index, verifying_hash); let participants = Verifications::::get(verification_key); diff --git a/pallets/exodus/src/lib.rs b/pallets/exodus/src/lib.rs index e879e24..54bcf19 100644 --- a/pallets/exodus/src/lib.rs +++ b/pallets/exodus/src/lib.rs @@ -31,7 +31,7 @@ use sp_io::hashing::blake2_256; use ghost_helpers::{ get_byzantium_quorum_threshold, SubstrateBlake2Hasher, - hash_chain::{cumulative_hash, sequential_hash}, + hash_chain::cumulative_hash, networks::{NetworkData, NetworkCurve, NetworkType}, bounded_bitmap::{validate_bitmap_sizes, BoundedBitmap}, }; @@ -40,7 +40,6 @@ use ghost_helpers::{ use ghost_traits::exodus::IdentifierConverter; use ghost_traits::{ - hashing::GhostHasher, bounded_bitmap::{ BoundedBitmapGenerator, BoundedBitmapWriter, BoundedBitmapReader, BoundedBitmapIterable, @@ -1254,11 +1253,9 @@ pub mod pallet { return Err(Error::::Round5PackageInvalidProof.into()); }; - let metadata_hashes = [ - SubstrateBlake2Hasher::hash(round5.merkle_root.as_ref()), - SubstrateBlake2Hasher::hash(round5.verifying_key.as_ref()), - ]; - let verifying_hash = sequential_hash::(metadata_hashes); + let metadata_hashes = [round5.merkle_root.as_ref(), round5.verifying_key.as_ref()]; + let verifying_hash = cumulative_hash::(metadata_hashes); + let state = QualificationDkgState::::get(&network_curve); ensure!(state.is_fifth_phase(), Error::::DkgWrongRound); From 84b6dfad9fdb6d5a4656e07ec1f340f1b506cca2 Mon Sep 17 00:00:00 2001 From: Uncle Stinky Date: Fri, 9 Oct 2026 13:23:23 +0300 Subject: [PATCH 3/4] fix for https://ghostforum.org/t/critical-utxo-multi-mint-via-non-canonical-varint-re-encoding/111 Signed-off-by: Uncle Stinky --- pallets/helpers/Cargo.toml | 2 +- pallets/helpers/src/utxo.rs | 9 +++ pallets/weaver/Cargo.toml | 2 +- pallets/weaver/src/tests.rs | 110 ++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) diff --git a/pallets/helpers/Cargo.toml b/pallets/helpers/Cargo.toml index 54d2641..61103e1 100644 --- a/pallets/helpers/Cargo.toml +++ b/pallets/helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-helpers" -version = "0.0.11" +version = "0.0.12" 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 04948e5..f0fb40a 100644 --- a/pallets/helpers/src/utxo.rs +++ b/pallets/helpers/src/utxo.rs @@ -357,6 +357,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result { .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); + if val < 0xfd { + return Err(VarintError::TypeConversionFailed); + } *bytes = &bytes[2..]; Ok(val as usize) } @@ -369,6 +372,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result { .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); + if val < 0x10000 { + return Err(VarintError::TypeConversionFailed); + } *bytes = &bytes[4..]; Ok(val as usize) } @@ -381,6 +387,9 @@ fn read_varint(bytes: &mut &[u8]) -> Result { .try_into() .map_err(|_| VarintError::TypeConversionFailed)?, ); + if val < 0x100000000 { + return Err(VarintError::TypeConversionFailed); + } *bytes = &bytes[8..]; Ok(val as usize) } diff --git a/pallets/weaver/Cargo.toml b/pallets/weaver/Cargo.toml index f474738..3b72278 100644 --- a/pallets/weaver/Cargo.toml +++ b/pallets/weaver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-weaver" -version = "0.0.14" +version = "0.0.15" description = "Weaving a secure cryptographic tapestry across different external chains." license.workspace = true authors.workspace = true diff --git a/pallets/weaver/src/tests.rs b/pallets/weaver/src/tests.rs index a6c2623..4fc1700 100644 --- a/pallets/weaver/src/tests.rs +++ b/pallets/weaver/src/tests.rs @@ -801,6 +801,116 @@ fn should_successfully_pull_utxo_thread_and_mint_currency() { }); } +#[test] +fn should_successfully_pull_utxo_thread_and_reject_nominal_varint_encoding() { + let pair = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed"); + let incoming_share = 0; + + let network_id = 69; + let network_data = NetworkDataBuilder::default() + .with_network_type(NetworkType::Utxo) + .with_incoming_share(incoming_share) + .with_gatekeeper( + // Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e + hex::decode("4635b1794a22bfbe6c5c5eba17b693f4aaf0e348") + .expect("Bytes32 from UTXO should be valid"), + ) + .build(); + + let authorities = vec![pair.clone()]; + let networks = vec![(network_id, network_data, Default::default())]; + + // Real world tx at block #481838 + // Block https://blockstream.info/block/0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a + // Transaction: https://blockstream.info/tx/4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e + + let payload = ( + // curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "gettxoutproof", "params": [["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e"], "0000000000000000011bdccb39e19e942a60edffdccd9779653c5b46b337904a"]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com + hex::decode("00000020375a14cfcf5730daa8ff8d7a5bd2cf59fba797e289c2e0000000000000000000529cd017146361b57e6b7353b16d8d38d32f92ee842039dd779d35bfbfa2f3e53f649e59e93c01187900ea9fb40600000cfbd7b718845d123b1ae1a7fd3bce9c97acba097dc912c4ac67a920395db07ba4eef685960869ba70e0ca38890565c97dc11f1d7ae538e7b509069f81e38eff1a2e4a6b66fc4233d7d0580af4d195c7f0b274849f19d405dfc6b930bb9edd3f4a4c1f1e9fa3a336c50e8ea16039e4db4bef7ff5946e3a019937bdf626c2336d0e3926a83a575218f84e6dc55ffb9d0997a41e4b4d5425a85f22160c9f52362a95021a573ff96bb429bd80a8ec9dcb899971553efe91b6a0fe57c4694c3e8da8dc7f1babd514f2746da1ed06f08664baed853c334e0a3917d0c859b19ee14f448f77e624822070db3b606d5092027ca9ecf7e0243e6c151081021ebb3ddcdd28c1b8f9fdcffebd1962590477675f3d29d52602a1e7e941f772a3bbff42cf2eb6e74a1124bf424f767e294ae6253a21602c7c6b0381743dd6dcd57b025983e0e34375b24f67ac8beb00713d2f70594bbf797d5d94c061cb9965e6458f2f017f983d7ce37361fa13fda372f595dc1f22895ab93339457c72c480fed1c4d1164a242303ff3600").expect("Bytes32 from UTXO should be valid"), + // curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getrawtransaction", "params": ["4a3fdd9ebb30b9c6df05d4199f8474b2f0c795d1f40a58d0d73342fc666b4a2e", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com + hex::decode("010000000299a43fae581f80baf44e021ddf7b11995f184db282d6e48c22248b9fe79c43bb010000008b483045022100e9db01784dcd879c65d66127c2d52a4f9244321d15796893d6dccadaa1146e3d02203d9bb80f8df8b66692d3d9517f7cbb898d7cc40a30b175432322e1956c9b973a0141048097e4f8bb8ee104d5c2ece11d42f7e4719df8ddba459ecadecaba87d233c81a3c14830371392dd8ed030ef970450c0b7af48d9db4ba20c046896481602eb9dbffffffffb3fd05bc6000a5a0384e9a613d452e9307568df0a964907068e00403a0a0e09e020000006a47304402205bb1cbad4b29421648c1e22cac32a56aa06c933fe655559823fffb71ffebbc0b022021648d970560aa5b2031560b52a0fd06305995a58a457663324c18c1a9b0bd0b8121020d6e50b2660af27933c42bc1395fe93df90ffac5e2a989f6a134919fb8cf8075ffffffff030000000000000000296a2769642b5bb666ac1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e7c150000000000001976a914c1c69d91a5719a23ba2094d89a4df3eadfa4aef788acefea2309000000001976a9144635b1794a22bfbe6c5c5eba17b693f4aaf0e34888ac00000000").expect("Bytes32 from UTXO should be valid"), + vec![ + // curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000e7da64065cc23d5c5a5aa402f088aaaf37c5fe1c3b6b92", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com + hex::decode("000000204a9037b3465b3c657997cddcffed602a949ee139cbdc1b010000000000000000669c3e2472384bc95993f154aada38d6b2113d8d32069e32e2724ba40915a1802a669e59e93c011801c8c735").expect("Bytes32 from UTXO should be valid"), + // curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["00000000000000000063da6d9362c0ac3b87877d1c5ad7afd5bd706571d5b393", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com + hex::decode("02000020926b3b1cfec537afaa88f002a45a5a5c3dc25c0664dae7000000000000000000af0f270bf3f868d47089f395197809c360f1506636c960d96afed68bbd72d0b4496d9e59e93c01183ac94b67").expect("Bytes32 from UTXO should be valid"), + // curl --data-binary '{"jsonrpc": "2.0", "id": "test-proof", "method": "getblockheader", "params": ["000000000000000000ec6063eb743366264368be7ff5021727682cb518963026", false]}' -H 'Content-Type: application/json' https://bitcoin-rpc.publicnode.com + hex::decode("0000002093b3d5716570bdd5afd75a1c7d87873bacc062936dda630000000000000000009ab437e7c66b10a59b67127f290d24f381aba4e03d5e16c062a356eea4d608dea56d9e59e93c0118264f310e").expect("Bytes32 from UTXO should be valid"), + ], + ); + + let encodings = vec![ + vec![0x02], // canonical + vec![0xFD, 0x02, 0x00], // non-minimal 3 bytes + vec![0xFE, 0x02, 0x00, 0x00, 0x00], // non-minimal 5 bytes + vec![0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // non-minimal 9 bytes + ]; + + new_test_ext(authorities, networks).execute_with(|| { + let session = 0; + let block_hash = + hex::decode("000000000000000000ec6063eb743366264368be7ff5021727682cb518963026") + .expect("Bitcoin Root hex should be valid"); + + let expected_root = H256::from_slice(&block_hash); + LoomStates::::insert(&network_id, &session, expected_root); + + let expected_receiver_bytes = hex::decode("1657090d02985f0cdb41436fe2d5a83a3acb1995d963a698e7e619e927bdcc5e").unwrap(); + let mut receiver_raw = [0u8; 32]; + receiver_raw.copy_from_slice(&expected_receiver_bytes); + + let receiver_account = sp_runtime::AccountId32::from(receiver_raw); + let transfer_amount: u64 = 153348847; + let mut expected_pure_amount: u64 = 0; + + for (idx, enc) in encodings.iter().enumerate() { + let mut variant = Vec::with_capacity(payload.1.len() + enc.len() - 1); + variant.extend_from_slice(&payload.1[0..4]); + variant.extend_from_slice(&enc); + variant.extend_from_slice(&payload.1[5..]); + + let utxo_thread_proof = UtxoThreadProof::new(payload.0.clone(), variant, payload.2.clone()); + let thread_info = ThreadProof::UtxoThreadProof(utxo_thread_proof); + let pulled_thread_key = thread_info.get_unique_key(session, network_id); + + assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount); + assert!(!PulledThreads::::contains_key( + &pulled_thread_key + )); + + let pull_result = GhostWeaver::pull_thread( + RuntimeOrigin::none(), + network_id, + session, + thread_info + ); + + if idx == 0 { + assert!(pull_result.is_ok()); + assert!(PulledThreads::::contains_key(&pulled_thread_key)); + + let curve_share = Perbill::from_parts(incoming_share).mul_ceil(transfer_amount); + expected_pure_amount += transfer_amount.saturating_sub(curve_share); + + assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount); + + System::assert_last_event(RuntimeEvent::GhostWeaver(Event::ThreadPulled { + pulled_thread_key, + network_id, + receiver: receiver_account.clone(), + amount: transfer_amount, + })); + } else { + assert!(pull_result.is_err()); + assert!(!PulledThreads::::contains_key(&pulled_thread_key)); + assert_eq!(Balances::free_balance(&receiver_account), expected_pure_amount); + } + } + + assert_eq!(Balances::free_balance(&receiver_account), transfer_amount); + }); +} + #[test] fn on_initialize_should_finalize_session_when_timeout_is_reached() { let pair1 = AuthorityPair::from_string("//Alice", None).expect("Should be valid seed"); From f3e7122d4351f941d1a9c7a43362b0e2b47a8840 Mon Sep 17 00:00:00 2001 From: Uncle Stinky Date: Fri, 9 Oct 2026 14:56:43 +0300 Subject: [PATCH 4/4] 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)?;