From d216a7cbe0491e36a408b7bb3028642f9f490533 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 22:12:46 +0100 Subject: [PATCH 1/7] 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 | 188 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 4 files changed, 303 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..42fd1884 --- /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: () = assert!(OFFSET + LEN <= N); +} + +struct Hack2; + +impl Hack2 { + const IS_FULL_RANGE: () = assert!(LEFT + RIGHT == N); +} 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..87fa6934 --- /dev/null +++ b/src/internals/slice.rs @@ -0,0 +1,188 @@ +//! 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: () = { + assert!(N != 0); + }; +} + +#[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 f90057ef..ea2da3aa 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 600572042c2648abb0fa86a4f903cd8b823f633f Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 12 Jun 2026 02:30:15 +0000 Subject: [PATCH 2/7] 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 | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/address.rs b/src/address.rs index 296fa63b..424838a6 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; @@ -489,35 +490,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 = data[0] == params.blinded_prefix; - let prefix = match (blinded, data.len()) { - (true, 55) => data[1], - (false, 21) => data[0], - (_, len) => return Err(AddressError::InvalidLength(len)), + 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 6659691f5efdb9840920b01b72632e2c66e461fc Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:14:36 +0100 Subject: [PATCH 3/7] 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 424a4e7d..71f7e899 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::{ @@ -775,15 +777,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 34f1a564..5475e8a7 100644 --- a/src/confidential.rs +++ b/src/confidential.rs @@ -730,6 +730,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 d6387617..38b95c42 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -74,6 +74,11 @@ impl AssetId { 0x28, 0x29, 0xc0, 0xd0, 0x57, 0x9f, 0x0a, 0x71, 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 { From acf5bc83a42899357bda4b228f3cfec9319618d8 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:17:47 +0100 Subject: [PATCH 4/7] transaction: use better error typing for pegin destructuring We should redo the error types here as well at some point. --- src/transaction.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/transaction.rs b/src/transaction.rs index 5af11ae6..323dd8d7 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -21,6 +21,8 @@ use std::convert::TryFrom; use bitcoin::{self, VarInt}; use crate::hashes::{Hash, sha256}; +use crate::internals::slice::SliceExt; + 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 6e082a923d18a7b2c947df820c1d9ec6eaf5f4c0 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:18:57 +0100 Subject: [PATCH 5/7] 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 424838a6..27fccb9e 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; @@ -471,13 +472,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 4428361587088bb192efd2e7f55a9835dfd904b0 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:20:22 +0100 Subject: [PATCH 6/7] 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 ed8af56c..4702f8c8 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 = Vec::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 6fee387ace9716afd32b51bdae696117d540cb01 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Mon, 15 Jun 2026 23:53:57 +0100 Subject: [PATCH 7/7] Bump version to 0.26.2 --- Cargo-latest.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo-latest.lock b/Cargo-latest.lock index 426e4104..180d9781 100644 --- a/Cargo-latest.lock +++ b/Cargo-latest.lock @@ -190,7 +190,7 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elements" -version = "0.26.1" +version = "0.26.2" dependencies = [ "bech32", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 5b5f627f..a644862b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "elements" -version = "0.26.1" +version = "0.26.2" 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"