From 8cd2a4d1aaaa00abf4a550c53683124e1df1be78 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:12:46 +0100 Subject: [PATCH 01/13] Add arrays.rs and slices.rs from bitcoin-internals 0.5 --- src/internals/array.rs | 107 ++++++++++++++++++++++++ src/internals/mod.rs | 5 ++ src/internals/slice.rs | 186 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 4 files changed, 301 insertions(+) create mode 100644 src/internals/array.rs create mode 100644 src/internals/mod.rs create mode 100644 src/internals/slice.rs diff --git a/src/internals/array.rs b/src/internals/array.rs new file mode 100644 index 00000000..f4de0ff7 --- /dev/null +++ b/src/internals/array.rs @@ -0,0 +1,107 @@ +//! Contains extensions related to arrays. + +use std::convert::TryInto; + +/// Extension trait for arrays. +pub trait ArrayExt { + /// The item type the array is storing. + type Item; + + /// Just like the slicing operation, this returns an array `LEN` items long at position + /// `OFFSET`. + /// + /// The correctness of this operation is compile-time checked. + /// + /// Note that unlike slicing where the second number is the end index, here the second number + /// is array length! + fn sub_array(&self) -> &[Self::Item; LEN]; + + /// Returns an item at given statically-known index. + /// + /// This is just like normal indexing except the check happens at compile time. + fn get_static(&self) -> &Self::Item { &self.sub_array::()[0] } + + /// Returns the first item in an array. + /// + /// Fails to compile if the array is empty. + /// + /// Note that this method's name intentionally shadows the `std`'s `first` method which + /// returns `Option`. The rationale is that given the known length of the array, we always know + /// that this will not return `None` so trying to keep the `std` method around is pointless. + /// Importing the trait will also cause compile failures - that's also intentional to expose + /// the places where useless checks are made. + fn first(&self) -> &Self::Item { self.get_static::<0>() } + + /// Splits the array into two, non-overlapping smaller arrays covering the entire range. + /// + /// This is almost equivalent to just calling [`sub_array`](Self::sub_array) twice, except it also + /// checks that the arrays don't overlap and that they cover the full range. This is very useful + /// for demonstrating correctness, especially when chained. Using this technique even revealed + /// a bug in the past. ([#4195](https://github.com/rust-bitcoin/rust-bitcoin/issues/4195)) + fn split_array( + &self, + ) -> (&[Self::Item; LEFT], &[Self::Item; RIGHT]); + + /// Splits the array into the first element and the remaining, one element shorter, array. + /// + /// Fails to compile if the array is empty. + /// + /// Note that this method's name intentionally shadows the `std`'s `split_first` method which + /// returns `Option`. The rationale is that given the known length of the array, we always know + /// that this will not return `None` so trying to keep the `std` method around is pointless. + /// Importing the trait will also cause compile failures - that's also intentional to expose + /// the places where useless checks are made. + fn split_first(&self) -> (&Self::Item, &[Self::Item; RIGHT]) { + let (first, remaining) = self.split_array::<1, RIGHT>(); + (&first[0], remaining) + } + + /// Splits the array into the last element and the remaining, one element shorter, array. + /// + /// Fails to compile if the array is empty. + /// + /// Note that this method's name intentionally shadows the `std`'s `split_last` method which + /// returns `Option`. The rationale is that given the known length of the array, we always know + /// that this will not return `None` so trying to keep the `std` method around is pointless. + /// Importing the trait will also cause compile failures - that's also intentional to expose + /// the places where useless checks are made. + /// + /// The returned tuple is also reversed just as `std` for consistency and simpler diffs when + /// migrating. + fn split_last(&self) -> (&Self::Item, &[Self::Item; LEFT]) { + let (remaining, last) = self.split_array::(); + (&last[0], remaining) + } +} + +impl ArrayExt for [T; N] { + type Item = T; + + fn sub_array(&self) -> &[Self::Item; LEN] { + #[allow(clippy::let_unit_value)] + let () = Hack::::IS_VALID_RANGE; + + self[OFFSET..(OFFSET + LEN)].try_into().expect("this is also compiler-checked above") + } + + fn split_array( + &self, + ) -> (&[Self::Item; LEFT], &[Self::Item; RIGHT]) { + #[allow(clippy::let_unit_value)] + let () = Hack2::::IS_FULL_RANGE; + + (self.sub_array::<0, LEFT>(), self.sub_array::()) + } +} + +struct Hack; + +impl Hack { + const IS_VALID_RANGE: () = [()][(OFFSET + LEN > N) as usize]; +} + +struct Hack2; + +impl Hack2 { + const IS_FULL_RANGE: () = [()][(LEFT + RIGHT != N) as usize]; +} diff --git a/src/internals/mod.rs b/src/internals/mod.rs new file mode 100644 index 00000000..443274a3 --- /dev/null +++ b/src/internals/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! These modules are copied from the bitcoin-internals 0.5 crate from rust-bitcoin +pub mod array; +pub mod slice; \ No newline at end of file diff --git a/src/internals/slice.rs b/src/internals/slice.rs new file mode 100644 index 00000000..3cf1eb91 --- /dev/null +++ b/src/internals/slice.rs @@ -0,0 +1,186 @@ +//! Contains extensions related to slices. + +use std::convert::TryInto; + +/// Extension trait for slice. +pub trait SliceExt { + /// The item type the slice is storing. + type Item; + + /// Splits up the slice into a slice of arrays and a remainder. + /// + /// Note that `N` must not be zero: + /// + /// ```ignore + /// # use bitcoin_internals::slice::SliceExt; + /// let slice = [1, 2, 3]; + /// let _fail = slice.bitcoin_as_chunks::<0>(); // Fails to compile + /// ``` + fn bitcoin_as_chunks(&self) -> (&[[Self::Item; N]], &[Self::Item]); + + /// Splits up the slice into a slice of arrays and a remainder. + /// + /// Note that `N` must not be zero: + /// + /// ```ignore + /// # use bitcoin_internals::slice::SliceExt; + /// let mut slice = [1, 2, 3]; + /// let _fail = slice.bitcoin_as_chunks_mut::<0>(); // Fails to compile + /// ``` + fn bitcoin_as_chunks_mut( + &mut self, + ) -> (&mut [[Self::Item; N]], &mut [Self::Item]); + + /// Tries to access a sub-array of length `ARRAY_LEN` at the specified `offset`. + /// + /// Returns `None` in case of out-of-bounds access. + fn get_array(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]>; + + /// Splits the slice into an array and remainder if it's long enough. + /// + /// Returns `None` if the slice is shorter than `ARRAY_LEN` + #[allow(clippy::type_complexity)] // it's not really complex and redefining would make it + // harder to understand + fn split_first_chunk( + &self, + ) -> Option<(&[Self::Item; ARRAY_LEN], &[Self::Item])>; + + /// Splits the slice into a remainder and an array if it's long enough. + /// + /// Returns `None` if the slice is shorter than `ARRAY_LEN` + #[allow(clippy::type_complexity)] // it's not really complex and redefining would make it + // harder to understand + fn split_last_chunk( + &self, + ) -> Option<(&[Self::Item], &[Self::Item; ARRAY_LEN])>; +} + +impl SliceExt for [T] { + type Item = T; + + fn bitcoin_as_chunks(&self) -> (&[[Self::Item; N]], &[Self::Item]) { + #[allow(clippy::let_unit_value)] + let () = Hack::::IS_NONZERO; + + let chunks_count = self.len() / N; + let total_left_len = chunks_count * N; + let (left, right) = self.split_at(total_left_len); + // SAFETY: we've obtained the pointer from a slice that's still live + // we're merely casting, so no aliasing issues here + // arrays of T have same alignment as T + // the resulting slice points within the obtained slice as was computed above + let left = unsafe { + core::slice::from_raw_parts(left.as_ptr().cast::<[Self::Item; N]>(), chunks_count) + }; + (left, right) + } + + fn bitcoin_as_chunks_mut( + &mut self, + ) -> (&mut [[Self::Item; N]], &mut [Self::Item]) { + #[allow(clippy::let_unit_value)] + let () = Hack::::IS_NONZERO; + + let chunks_count = self.len() / N; + let total_left_len = chunks_count * N; + let (left, right) = self.split_at_mut(total_left_len); + // SAFETY: we've obtained the pointer from a slice that's still live + // we're merely casting, so no aliasing issues here + // arrays of T have same alignment as T + // the resulting slice points within the obtained slice as was computed above + let left = unsafe { + core::slice::from_raw_parts_mut( + left.as_mut_ptr().cast::<[Self::Item; N]>(), + chunks_count, + ) + }; + (left, right) + } + + fn get_array(&self, offset: usize) -> Option<&[Self::Item; ARRAY_LEN]> { + self.get(offset..(offset + ARRAY_LEN)).map(|slice| { + slice + .try_into() + .expect("the arguments to `get` evaluate to the same length the return type uses") + }) + } + + fn split_first_chunk( + &self, + ) -> Option<(&[Self::Item; ARRAY_LEN], &[Self::Item])> { + if self.len() < ARRAY_LEN { + return None; + } + let (first, remainder) = self.split_at(ARRAY_LEN); + Some((first.try_into().expect("we're passing `ARRAY_LEN` to `split_at` above"), remainder)) + } + + fn split_last_chunk( + &self, + ) -> Option<(&[Self::Item], &[Self::Item; ARRAY_LEN])> { + if self.len() < ARRAY_LEN { + return None; + } + let (remainder, last) = self.split_at(self.len() - ARRAY_LEN); + Some(( + remainder, + last.try_into().expect("we're passing `self.len() - ARRAY_LEN` to `split_at` above"), + )) + } +} + +struct Hack; + +impl Hack { + const IS_NONZERO: () = [()][(N == 0) as usize]; +} + +#[cfg(test)] +mod tests { + use super::SliceExt; + + // some comparisons require type annotations + const EMPTY: &[i32] = &[]; + + #[test] + fn one_to_one() { + let slice = [1]; + let (left, right) = slice.bitcoin_as_chunks::<1>(); + assert_eq!(left, &[[1]]); + assert_eq!(right, EMPTY); + } + + #[test] + fn one_to_two() { + const EMPTY_LEFT: &[[i32; 2]] = &[]; + + let slice = [1i32]; + let (left, right) = slice.bitcoin_as_chunks::<2>(); + assert_eq!(left, EMPTY_LEFT); + assert_eq!(right, &[1]); + } + + #[test] + fn two_to_one() { + let slice = [1, 2]; + let (left, right) = slice.bitcoin_as_chunks::<1>(); + assert_eq!(left, &[[1], [2]]); + assert_eq!(right, EMPTY); + } + + #[test] + fn two_to_two() { + let slice = [1, 2]; + let (left, right) = slice.bitcoin_as_chunks::<2>(); + assert_eq!(left, &[[1, 2]]); + assert_eq!(right, EMPTY); + } + + #[test] + fn three_to_two() { + let slice = [1, 2, 3]; + let (left, right) = slice.bitcoin_as_chunks::<2>(); + assert_eq!(left, &[[1, 2]]); + assert_eq!(right, &[3]); + } +} diff --git a/src/lib.rs b/src/lib.rs index b54fc1f5..37c0d9cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,9 @@ pub mod taproot; mod transaction; // consider making upstream public mod endian; +#[allow(dead_code)] +mod internals; + // re-export bitcoin deps which we re-use pub use bitcoin::hashes; // export everything at the top level so it can be used as `elements::Transaction` etc. From 179c6dac4d710096d86aa7bf55a809f1570af097 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 20:58:23 +0100 Subject: [PATCH 02/13] rewrite Address::from_base58 to eliminate all the unwraps These unwraps and indexing (which hide more panic paths) irritated me, and also would need to be tweaked once we get rid of the Hash::from_slice methods. I figured I'd preemptively get rid of them. --- src/address.rs | 53 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/address.rs b/src/address.rs index e4b5fda3..7419fec4 100644 --- a/src/address.rs +++ b/src/address.rs @@ -26,6 +26,7 @@ use crate::blech32::{Blech32, Blech32m}; use crate::hashes::Hash; use bitcoin::base58; use bitcoin::PublicKey; +use crate::internals::array::ArrayExt as _; use secp256k1_zkp; use secp256k1_zkp::Secp256k1; use secp256k1_zkp::Verification; @@ -487,43 +488,41 @@ impl Address { // data.len() should be >= 1 when this method is called fn from_base58(data: &[u8], params: &'static AddressParams) -> Result { + let len_error = AddressError::InvalidLength(data.len()); // When unblinded, the structure is: // <1: regular prefix> <20: hash160> // When blinded, the structure is: // <1: blinding prefix> <1: regular prefix> <33: blinding pubkey> <20: hash160> - let (blinded, prefix) = match data[0] == params.blinded_prefix { - true => { - if data.len() != 55 { - return Err(AddressError::InvalidLength(data.len())); - } - (true, data[1]) - } - false => { - if data.len() != 21 { - return Err(AddressError::InvalidLength(data.len())); - } - (false, data[0]) - } + let (blinding_prefix, blinded_data) = match data.split_first() { + Some(v) => v, + None => return Err(len_error), }; - let (blinding_pubkey, payload_data) = match blinded { - true => ( - Some( - secp256k1_zkp::PublicKey::from_slice(&data[2..35]) - .map_err(AddressError::InvalidBlindingPubKey)?, - ), - &data[35..], - ), - false => (None, &data[1..]), + let (prefix, blinding_pubkey, hash) = if *blinding_prefix == params.blinded_prefix { + let (prefix, pubkey_and_hash) = match blinded_data.split_first() { + Some(v) => v, + None => return Err(len_error), + }; + + let pubkey_and_hash = <&[u8; 53]>::try_from(pubkey_and_hash).map_err(|_| len_error)?; + let (pubkey, hash) = pubkey_and_hash.split_array::<33, 20>(); + + let blinding_pubkey = secp256k1_zkp::PublicKey::from_slice(pubkey) + .map_err(AddressError::InvalidBlindingPubKey)?; + + (prefix, Some(blinding_pubkey), hash) + } else { + let hash = <&[u8; 20]>::try_from(blinded_data).map_err(|_| len_error)?; + (blinding_prefix, None, hash) }; - let payload = if prefix == params.p2pkh_prefix { - Payload::PubkeyHash(PubkeyHash::from_slice(payload_data).unwrap()) - } else if prefix == params.p2sh_prefix { - Payload::ScriptHash(ScriptHash::from_slice(payload_data).unwrap()) + let payload = if *prefix == params.p2pkh_prefix { + Payload::PubkeyHash(PubkeyHash::from_byte_array(*hash)) + } else if *prefix == params.p2sh_prefix { + Payload::ScriptHash(ScriptHash::from_byte_array(*hash)) } else { - return Err(AddressError::InvalidAddressVersion(prefix)); + return Err(AddressError::InvalidAddressVersion(*prefix)); }; Ok(Address { From 8b156496218bc8fd3a8931076a77bee6b191f604 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 20:59:23 +0100 Subject: [PATCH 03/13] blind: use array splitting in TxOut::unblind (fix potential DoS?) I don't *think* it's possible to create a rangeproof with a sidechannel smaller than 64 bytes (if you create a 0-sized "proof of exact value" then unwinding will fail entirely, and anything larger I think has at least one ring, so 128 bytes or more). Unsure. But better not to assume this by indexing recklessly into the sidechannel message. --- src/blind.rs | 26 ++++++++++++++++++++++---- src/confidential.rs | 5 +++++ src/issuance.rs | 5 +++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/blind.rs b/src/blind.rs index 650fcd74..0e3e5f58 100644 --- a/src/blind.rs +++ b/src/blind.rs @@ -15,6 +15,8 @@ //! # Transactions Blinding //! +use crate::internals::array::ArrayExt as _; +use crate::internals::slice::SliceExt; use std::{self, collections::BTreeMap, fmt}; use secp256k1_zkp::{ @@ -774,15 +776,31 @@ impl TxOut { additional_generator, )?; - let (asset, asset_bf) = opening.message.as_ref().split_at(32); - let asset = AssetId::from_slice(asset)?; - let asset_bf = AssetBlindingFactor::from_slice(&asset_bf[..32])?; + // Use `MissingRangeproof` error because it's available so does not require + // API breaks. In a later PR we should extend that enum and add #[non_exhaustive] + // to it. The maybe-better `MalformedAssetId` error requires we start with a + // std `FromSliceError` which we don't have. + let asset_and_bf = SliceExt::split_first_chunk::<64>(opening.message.as_ref()) + .ok_or(UnblindError::MissingRangeproof)? + .0; + let (asset_id, asset_bf) = asset_and_bf.split_array(); + + let asset_id = AssetId::from_byte_array(*asset_id); + let asset_bf = AssetBlindingFactor::from_byte_array(*asset_bf)?; + if let Asset::Confidential(own_asset) = self.asset { + let secp = Secp256k1::signing_only(); // needed to avoid API break + let asset = Generator::new_blinded(&secp, asset_id.into_tag(), asset_bf.into_inner()); + if asset != own_asset { + // See above about use of MissingRangeproof. + return Err(UnblindError::MissingRangeproof); + } + } let value = opening.value; let value_bf = ValueBlindingFactor(opening.blinding_factor); Ok(TxOutSecrets { - asset, + asset: asset_id, asset_bf, value, value_bf, diff --git a/src/confidential.rs b/src/confidential.rs index ba6c8543..709ad103 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -746,6 +746,11 @@ impl AssetBlindingFactor { AssetBlindingFactor(Tweak::new(rng)) } + /// Create from bytes. + pub fn from_byte_array(bytes: [u8; 32]) -> Result { + Ok(AssetBlindingFactor(Tweak::from_inner(bytes)?)) + } + /// Create from bytes. pub fn from_slice(bytes: &[u8]) -> Result { Ok(AssetBlindingFactor(Tweak::from_slice(bytes)?)) diff --git a/src/issuance.rs b/src/issuance.rs index de478e7b..cc291e37 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -75,6 +75,11 @@ impl AssetId { 0x3d, 0x1c, 0x04, 0xed, 0xe9, 0x79, 0x02, 0x6f, ])); + /// Constructs this wrapper struct from raw bytes. + pub fn from_byte_array(inner: [u8; 32]) -> Self { + Self(sha256::Midstate::from_byte_array(inner)) + } + /// Create an [AssetId] from its inner type. pub const fn from_inner(midstate: sha256::Midstate) -> AssetId { AssetId(midstate) From 2a8b46f3a94084051129b9f7bfa95ca42e63ed80 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:28:00 +0100 Subject: [PATCH 04/13] transaction: use better error typing for pegin destructuring We should redo the error types here as well at some point. --- src/transaction.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/transaction.rs b/src/transaction.rs index f5973e84..aab29b12 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -20,7 +20,9 @@ use std::collections::HashMap; use std::convert::TryFrom; use bitcoin::{self, VarInt}; -use crate::hashes::{Hash, sha256}; +use bitcoin::hashes::sha256; +use crate::internals::slice::SliceExt; +use crate::hashes::Hash; use crate::{confidential, ContractHash}; use crate::encode::{self, Encodable, Decodable}; @@ -433,12 +435,14 @@ impl<'tx> PeginData<'tx> { pegin_witness: &'tx [Vec], prevout: bitcoin::OutPoint, ) -> Result, &'static str> { - if pegin_witness.len() != 6 { - return Err("size not 6"); - } - if pegin_witness[5].len() < 80 { - return Err("merkle proof too short"); - } + let pegin_witness = match <&[Vec; 6]>::try_from(pegin_witness) { + Ok(v) => v, + Err(_) => return Err("size not 6"), + }; + let (block_header, _) = match SliceExt::split_first_chunk::<80>(pegin_witness[5].as_slice()) { + Some(v) => v, + None => return Err("merkle proof too short"), + }; Ok(PeginData { outpoint: prevout, @@ -449,7 +453,7 @@ impl<'tx> PeginData<'tx> { claim_script: &pegin_witness[3], tx: &pegin_witness[4], merkle_proof: &pegin_witness[5], - referenced_block: bitcoin::BlockHash::hash(&pegin_witness[5][0..80]), + referenced_block: bitcoin::BlockHash::hash(block_header), }) } From b06dac91c046f90e871b3288912113b5332e58fb Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:31:20 +0100 Subject: [PATCH 05/13] address: do a better job slicing bech32 data Currently we allow decoding segwit v0 programs which have uncompressed/hybrid keys (not allowed) and I suspect that if you provide a too-short address then you'll get a panic here. --- src/address.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/address.rs b/src/address.rs index 7419fec4..5949993e 100644 --- a/src/address.rs +++ b/src/address.rs @@ -27,6 +27,7 @@ use crate::hashes::Hash; use bitcoin::base58; use bitcoin::PublicKey; use crate::internals::array::ArrayExt as _; +use crate::internals::slice::SliceExt; use secp256k1_zkp; use secp256k1_zkp::Secp256k1; use secp256k1_zkp::Verification; @@ -469,13 +470,17 @@ impl Address { }; let (blinding_pubkey, program) = match blinded { - true => ( + true => { + let (pk, rest) = SliceExt::split_first_chunk::<33>(data.as_slice()) + .ok_or(AddressError::InvalidSegwitV0Encoding)?; + ( Some( - secp256k1_zkp::PublicKey::from_slice(&data[..33]) + secp256k1_zkp::PublicKey::from_slice(pk) .map_err(AddressError::InvalidBlindingPubKey)?, - ), - data[33..].to_vec(), - ), + ), + rest.to_vec(), + ) + }, false => (None, data), }; From 250c4f664a67feb8e8b80984bb626ca1eba4f8a2 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:32:00 +0100 Subject: [PATCH 06/13] pset: fix slicing in KeySource::deserialize Lol, this code was so close and yet so far. pset --- src/pset/serialize.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pset/serialize.rs b/src/pset/serialize.rs index 87062d55..5c53e6b5 100644 --- a/src/pset/serialize.rs +++ b/src/pset/serialize.rs @@ -17,7 +17,6 @@ //! Defines traits used for (de)serializing PSET values into/from raw //! bytes in PSET key-value pairs. -use std::convert::TryFrom; use std::io; use crate::confidential::{self, AssetBlindingFactor}; @@ -30,6 +29,7 @@ use crate::{AssetId, BlockHash, Script, Transaction, TxOut, Txid}; use bitcoin; use bitcoin::bip32::{ChildNumber, Fingerprint, KeySource}; use bitcoin::{key::XOnlyPublicKey, PublicKey}; +use crate::internals::slice::SliceExt; use secp256k1_zkp::{self, RangeProof, SurjectionProof, Tweak}; use super::map::{PsbtSighashType, TapTree}; @@ -176,20 +176,17 @@ impl Serialize for KeySource { impl Deserialize for KeySource { fn deserialize(bytes: &[u8]) -> Result { - let prefix = match <[u8; 4]>::try_from(&bytes[0..4]) { - Ok(prefix) => prefix, - Err(_) => return Err(io::Error::from(io::ErrorKind::UnexpectedEof).into()), + let (prefix, mut rest) = match SliceExt::split_first_chunk::<4>(bytes) { + Some(v) => v, + None => return Err(io::Error::from(io::ErrorKind::UnexpectedEof).into()), }; let fprint: Fingerprint = Fingerprint::from(prefix); let mut dpath: Vec = Default::default(); - let mut d = &bytes[4..]; - while !d.is_empty() { - match u32::consensus_decode(&mut d) { - Ok(index) => dpath.push(index.into()), - Err(e) => return Err(e), - } + while !rest.is_empty() { + let index = u32::consensus_decode(&mut rest)?; + dpath.push(index.into()); } Ok((fprint, dpath.into())) From 2941a3993e43a6fe1d256f8e42986fd49361d005 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:47:11 +0100 Subject: [PATCH 07/13] Remove `-- -D warnings` from test.sh --- contrib/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/test.sh b/contrib/test.sh index e41611f8..349143aa 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -38,18 +38,18 @@ fi if [ "$DO_LINT" = true ] then - cargo clippy --all-features --all-targets -- -D warnings + cargo clippy --all-features --all-targets fi # Build the docs if told to (this only works with the nightly toolchain) if [ "$DO_DOCSRS" = true ]; then - RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features + RUSTDOCFLAGS="--cfg docsrs -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features fi # Build the docs with a stable toolchain, in unison with the DO_DOCSRS command # above this checks that we feature guarded docs imports correctly. if [ "$DO_DOCS" = true ]; then - RUSTDOCFLAGS="-D warnings" cargo +stable doc --all-features + cargo +stable doc --all-features fi From 70276cc4bede4e37157c0f57da34d6f7b5935f20 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:54:35 +0100 Subject: [PATCH 08/13] pin bitcoin crates in test.sh --- contrib/test.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contrib/test.sh b/contrib/test.sh index 349143aa..1adc3dda 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -13,6 +13,17 @@ if cargo --version | grep "1\.56"; then cargo update -p serde_json --precise 1.0.98 cargo update -p serde --precise 1.0.156 cargo update -p ppv-lite86 --precise 0.2.8 + cargo update -p bitcoin --precise 0.32.2 + cargo update -p bitcoin-units --precise 0.1.2 + cargo update -p bitcoin-io --precise 0.1.2 + cargo update -p bitcoin_hashes --precise 0.14.2 + cargo update -p base58ck --precise 0.1.0 + cargo update -p itoa --precise 1.0.6 + cargo update -p ryu --precise 1.0.9 + cargo update -p getrandom --precise 0.2.3 + cargo update -p quote --precise 1.0.35 + cargo update -p proc-macro2 --precise 1.0.80 + cargo update -p unicode-ident --precise 1.0.6 fi if [ "$DO_FEATURE_MATRIX" = true ] From ea42e110e50d24610e477f2c64d8d6b6e2ab456f Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:46:17 +0100 Subject: [PATCH 09/13] add `--no-deps` to doc test --- contrib/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/test.sh b/contrib/test.sh index 1adc3dda..ad485aa6 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -54,7 +54,7 @@ fi # Build the docs if told to (this only works with the nightly toolchain) if [ "$DO_DOCSRS" = true ]; then - RUSTDOCFLAGS="--cfg docsrs -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features + RUSTDOCFLAGS="--cfg docsrs -D rustdoc::broken-intra-doc-links" cargo +nightly doc --no-deps --all-features fi # Build the docs with a stable toolchain, in unison with the DO_DOCSRS command From 675ca264731a73231deb86d7552807f17584be89 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:46:32 +0100 Subject: [PATCH 10/13] Pin deps for Fuzztest job --- contrib/test.sh | 17 ++++++++++++++++- fuzz/travis-fuzz.sh | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/contrib/test.sh b/contrib/test.sh index ad485aa6..81122628 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -69,8 +69,23 @@ if [ "$DO_FUZZ" = true ] then ( cd fuzz - if cargo --version | grep "1\.58"; then + if cargo --version | grep "1\.63"; then cargo update -p cc --precise 1.0.94 + cargo update -p bitcoin --precise 0.32.2 + cargo update -p bitcoin-units --precise 0.1.2 + cargo update -p bitcoin-io --precise 0.1.2 + cargo update -p bitcoin_hashes --precise 0.14.2 + cargo update -p base58ck --precise 0.1.0 + cargo update -p itoa --precise 1.0.6 + cargo update -p unicode-ident --precise 1.0.6 + cargo update -p serde_json --precise 1.0.98 + cargo update -p serde --precise 1.0.156 + cargo update -p ppv-lite86 --precise 0.2.8 + cargo update -p quote --precise 1.0.28 + cargo update -p proc-macro2 --precise 1.0.66 + cargo update -p libc --precise 0.2.163 + cargo update -p ryu --precise 1.0.9 + cargo update -p honggfuzz --precise 0.5.54 fi cargo test --verbose ./travis-fuzz.sh diff --git a/fuzz/travis-fuzz.sh b/fuzz/travis-fuzz.sh index 1b099884..be756db0 100755 --- a/fuzz/travis-fuzz.sh +++ b/fuzz/travis-fuzz.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -cargo install --force honggfuzz --no-default-features +cargo install --force honggfuzz --no-default-features --version 0.5.54 for TARGET in fuzz_targets/*; do FILENAME=$(basename $TARGET) FILE="${FILENAME%.*}" From 6275989726275e1d3e4c569f2da702b163a17b0e Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:50:28 +0100 Subject: [PATCH 11/13] Fix docstring in blech32 --- src/blech32/decode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blech32/decode.rs b/src/blech32/decode.rs index b73c80d1..39795e57 100644 --- a/src/blech32/decode.rs +++ b/src/blech32/decode.rs @@ -82,7 +82,7 @@ const SEP: char = '1'; /// checksum in any way. /// /// Unless you are attempting to validate a string with multiple checksums then you likely do not -/// want to use this type directly, instead use [`CheckedHrpstring::new(s)`]. +/// want to use this type directly, instead use [`CheckedHrpstring::new`]. #[derive(Debug)] pub struct UncheckedHrpstring<'s> { /// The human-readable part, guaranteed to be lowercase ASCII characters. From ad005636cfececcff816c3e58c36a21b4e985001 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:57:14 +0100 Subject: [PATCH 12/13] Bump version to 0.25.3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 12029a50..1895d585 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "elements" -version = "0.25.2" +version = "0.25.3" authors = ["Andrew Poelstra "] description = "Library with support for de/serialization, parsing and executing on data structures and network messages related to Elements" license = "CC0-1.0" From 982a50f599e72b72c95908f72d4a88d65793f7aa Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Tue, 16 Jun 2026 00:37:10 +0100 Subject: [PATCH 13/13] Disable fuzztest in CI --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b1b5fe76..6627386b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -84,7 +84,7 @@ jobs: env: DO_DOCS: false DO_DOCSRS: false - DO_FUZZ: true + DO_FUZZ: false DO_INTEGRATION: false DO_LINT: false DO_FEATURE_MATRIX: false