From dca9bb001e824649259af0acd4dcff3ff9f02a06 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 24 Jun 2026 15:18:01 +0000 Subject: [PATCH 01/14] update bitcoin dependency to 0.32.102, hashes to 1.1 The 0.32.100 series has a higher MSRV (but still lower than ours) and introduces the bitcoin-consensus-encoding crate. Using this, we can eliminate all the bitcoin-io junk from this crate, eliminate our own Encoder trait, eliminate its passthrough impls on bitcoin types, etc. Since bitcoin depends on `encoding 1.0` but we will need 1.1, also add a separate explict encoding dependency. By updating bitcoin and hashes together, we can eliminate the duplicate `bitcoin-internals` dependency; everything is on 0.6 now. --- Cargo-recent.lock | 72 ++++++++++++++++++++++------------------------- Cargo.toml | 8 +++--- src/lib.rs | 2 ++ 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/Cargo-recent.lock b/Cargo-recent.lock index f83c2120..54c72aa2 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -22,12 +22,11 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "base58ck" -version = "0.1.0" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" dependencies = [ - "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.101", ] [[package]] @@ -59,18 +58,18 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.6" +version = "0.32.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8929a18b8e33ea6b3c09297b687baaa71fb1b97353243a3f1029fad5c59c5b" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" dependencies = [ "base58ck", "base64 0.21.7", "bech32", - "bitcoin-internals 0.3.0", + "bitcoin-consensus-encoding", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes 0.14.0", - "hex-conservative 0.2.1", + "bitcoin_hashes 0.14.101", + "hex-conservative 0.2.2", "hex_lit", "secp256k1", "serde", @@ -78,33 +77,29 @@ dependencies = [ [[package]] name = "bitcoin-consensus-encoding" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d6094e2a1ba3c93b5a596fe5a10d1a10c3c6e06785cde89f693a044c01aa40" -dependencies = [ - "bitcoin-internals 0.5.0", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" dependencies = [ + "bitcoin-internals", + "hex-conservative 1.1.0", "serde", ] [[package]] name = "bitcoin-internals" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] name = "bitcoin-private" @@ -114,33 +109,33 @@ checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" [[package]] name = "bitcoin-units" -version = "0.1.2" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" dependencies = [ - "bitcoin-internals 0.3.0", + "bitcoin-consensus-encoding", "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.1", + "hex-conservative 0.2.2", "serde", ] [[package]] name = "bitcoin_hashes" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f70c29ac06e7effa19682e91318deae86bdb46c4fd1bbd0f12fd196ff427ab0" +checksum = "a67800fcf7f3ca52f7796d4466948a424326b225950c79d1e76d0458760bb25d" dependencies = [ "bitcoin-consensus-encoding", - "bitcoin-internals 0.5.0", + "bitcoin-internals", "hex-conservative 1.1.0", "serde", ] @@ -230,8 +225,9 @@ dependencies = [ "bech32", "bincode", "bitcoin", - "bitcoin-internals 0.5.0", - "bitcoin_hashes 1.0.0", + "bitcoin-consensus-encoding", + "bitcoin-internals", + "bitcoin_hashes 1.1.0", "getrandom 0.2.16", "hex-conservative 1.1.0", "rand", @@ -313,9 +309,9 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec", ] @@ -512,7 +508,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes 0.14.101", "rand", "secp256k1-sys", "serde", diff --git a/Cargo.toml b/Cargo.toml index 256c2c16..cfe143ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,10 @@ base64 = ["bitcoin/base64"] [dependencies] bech32 = "0.11.0" -bitcoin = "0.32.2" -hashes = { package = "bitcoin_hashes", version = "1.0", features = [ "hex" ] } -internals = { package = "bitcoin-internals", version = "0.5" } +bitcoin = { version = "0.32.102", default-features = false, features = [ "encoding", "std" ] } +encoding = { package = "bitcoin-consensus-encoding", version = "1.1.0", default-features = false, features = [ "hex", "std" ] } +hashes = { package = "bitcoin_hashes", version = "1.1", features = [ "hex" ] } +internals = { package = "bitcoin-internals", version = "0.6" } secp256k1-zkp = { version = "0.11.0", features = ["global-context", "hashes"] } # Used for ContractHash::from_json_contract. @@ -191,7 +192,6 @@ zero_sized_map_values = "warn" [package.metadata.rbmt.lint] allowed_duplicates = [ "bitcoin_hashes", - "bitcoin-internals", "hex-conservative", ] diff --git a/src/lib.rs b/src/lib.rs index 18c92cea..21543b1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ /// Re-export of bitcoin crate pub extern crate bitcoin; +/// Re-export of bitcoin-consensus-encoding crate +pub extern crate encoding; /// Re-export of `bitcoin_hashes` crate pub extern crate hashes; /// Re-export of hex crate From 6baad08d736f15aff308340e76425e828bdc1ead Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 16 Jul 2026 13:25:20 +0000 Subject: [PATCH 02/14] hashes: remove deprecated macro This macro provided very little savings in lines of code, and wasn't worth the obfuscation. --- src/taproot.rs | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/taproot.rs b/src/taproot.rs index e6496cca..4bffd3fd 100644 --- a/src/taproot.rs +++ b/src/taproot.rs @@ -20,21 +20,41 @@ use crate::hashes::HashEngine as _; use crate::schnorr::{UntweakedPublicKey, TweakedPublicKey, TapTweak}; use crate::Script; use std::collections::{BTreeMap, BTreeSet, BinaryHeap}; -use hashes::sha256t; +use hashes::{sha256, sha256t}; use secp256k1_zkp::{self, Secp256k1, Scalar}; use crate::encode::Encodable; -hashes::sha256t_tag! { - pub struct TapLeafTag = hash_str("TapLeaf/elements"); + +/// The tag for tapleaves. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapLeafTag; + +/// The tag for tapbranches. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapBranchTag; + +/// The tag for taptweaks. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapTweakTag; + +/// The tag for Taproot sighashes. +#[derive(Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord, Hash)] +pub struct TapSighashTag; + +impl sha256t::Tag for TapLeafTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapLeaf/elements"); } -hashes::sha256t_tag! { - pub struct TapBranchTag = hash_str("TapBranch/elements"); + +impl sha256t::Tag for TapBranchTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapBranch/elements"); } -hashes::sha256t_tag! { - pub struct TapTweakTag = hash_str("TapTweak/elements"); + +impl sha256t::Tag for TapTweakTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapTweak/elements"); } -hashes::sha256t_tag! { - pub struct TapSighashTag = hash_str("TapSighash/elements"); + +impl sha256t::Tag for TapSighashTag { + const MIDSTATE: sha256::Midstate = sha256::Midstate::hash_tag(b"TapSighash/elements"); } // Taproot test vectors from BIP-341 state the hashes without any reversing From aacd08149308ed946a7c64790d39a5511d30187e Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 24 Jun 2026 23:25:13 +0000 Subject: [PATCH 03/14] confidential: implement Encode/Decode for confidential commitments This commit introduces the `decoder_state_machine` macro which generates (a lot of) the boilerplate needed to implement the encoding::Decoder trait. This includes internal enums, wrapper types to hide the internal enums, the state machine accounting, impls of std::error::Error, etc etc. I got Claude 4 to write a detailed doccomment, from which I deleted a bunch of fluff. But all the code in the macro was hand-written based on my implementing this decoder state machine a dozen times across the codebase. (My original implementations are thrown away and not included in this PR.) --- src/confidential/asset.rs | 112 +++++++++++++++++ src/confidential/mod.rs | 74 ++++++++++- src/confidential/nonce.rs | 112 +++++++++++++++++ src/confidential/value.rs | 112 +++++++++++++++++ src/internal_macros.rs | 258 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 663 insertions(+), 5 deletions(-) diff --git a/src/confidential/asset.rs b/src/confidential/asset.rs index d419ebde..5636b4bf 100644 --- a/src/confidential/asset.rs +++ b/src/confidential/asset.rs @@ -10,7 +10,9 @@ use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::issuance::AssetId; type ExplicitInner = AssetId; @@ -18,6 +20,7 @@ type ConfInner = Generator; const EXPLICIT_LEN: usize = 32; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x0a; const CONF_PREFIX_2: u8 = 0x0b; @@ -344,3 +347,112 @@ impl<'de> Deserialize<'de> for BlindingFactor { } } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Asset`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Asset { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit32(Some(1), id.as_byte_array()), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Asset`] type. + pub struct Decoder(enum DecoderInner { + Done(Asset), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Asset::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Asset::Explicit(AssetId::from_byte_array(bytes)))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Asset::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Asset`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(secp256k1_zkp::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index b6dbe807..335a179d 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -25,17 +25,68 @@ mod range_proof; mod surjection_proof; mod value; -use core::fmt; +use core::{fmt, slice}; use secp256k1_zkp; -pub use self::asset::{Asset, BlindingFactor as AssetBlindingFactor}; -pub use self::nonce::Nonce; +pub use self::asset::{ + Asset, BlindingFactor as AssetBlindingFactor, Decoder as AssetDecoder, + DecoderError as AssetDecoderError, Encoder as AssetEncoder, +}; +pub use self::nonce::{ + Decoder as NonceDecoder, DecoderError as NonceDecoderError, Encoder as NonceEncoder, Nonce, +}; pub use self::range_proof::RangeProof; pub use self::surjection_proof::SurjectionProof; -pub use self::value::{BlindingFactor as ValueBlindingFactor, Value}; -use crate::encode; +pub use self::value::{ + BlindingFactor as ValueBlindingFactor, Decoder as ValueDecoder, + DecoderError as ValueDecoderError, Encoder as ValueEncoder, Value, +}; use crate::issuance::AssetId; +use crate::{encode, encoding}; + +#[derive(Clone, Debug)] +enum CommitmentEncoder<'e> { + Null(u8), + Explicit8(Option, [u8; 8]), + Explicit32(Option, &'e [u8; 32]), + Explicit33([u8; 33]), +} + +impl encoding::Encoder for CommitmentEncoder<'_> { + fn current_chunk(&self) -> &[u8] { + match *self { + Self::Null(ref prefix) => slice::from_ref(prefix), + Self::Explicit8(ref prefix, ref arr) => prefix.as_ref().map_or(arr, slice::from_ref), + Self::Explicit32(ref prefix, arr) => prefix.as_ref().map_or(arr, slice::from_ref), + Self::Explicit33(ref arr) => arr, + } + } + + fn advance(&mut self) -> encoding::EncoderStatus { + match *self { + Self::Explicit8(ref mut prefix @ Some(_), _) + | Self::Explicit32(ref mut prefix @ Some(_), _) => { + *prefix = None; + encoding::EncoderStatus::HasMore + } + _ => encoding::EncoderStatus::Finished, + } + } +} + +impl encoding::ExactSizeEncoder for CommitmentEncoder<'_> { + fn len(&self) -> usize { + match *self { + Self::Null(_) => 1, + Self::Explicit8(Some(_), _) => 9, + Self::Explicit8(None, _) => 8, + Self::Explicit32(Some(_), _) => 33, + Self::Explicit32(None, _) => 32, + Self::Explicit33(_) => 33, + } + } +} /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] @@ -96,6 +147,7 @@ mod tests { use super::*; use crate::encode::Encodable as _; + use crate::encoding; const VALUE_EXPLICIT: [u8; 9] = [1, 0, 0, 0, 0, 0, 0, 3, 232]; @@ -158,6 +210,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } let nonce_encodings = [ @@ -177,6 +232,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } let asset_encodings = [ @@ -196,6 +254,9 @@ mod tests { assert_eq!(v.consensus_encode(&mut x).unwrap(), v.encoded_length()); assert_eq!(x.len(), v.encoded_length()); assert_eq!(x, *enc); + + assert_eq!(encoding::encode_to_vec(v), *enc); + assert_eq!(encoding::decode_from_slice(enc), Ok(*v)); } } @@ -207,6 +268,7 @@ mod tests { assert_eq!(x, Value::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Value::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), VALUE_COMMITMENT1); let x = Asset::from_commitment(&ASSET_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); @@ -214,6 +276,7 @@ mod tests { assert_eq!(x, Asset::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Asset::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), ASSET_COMMITMENT1); let x = Nonce::from_commitment(&NONCE_COMMITMENT1).unwrap(); let commitment = x.commitment().unwrap(); @@ -221,6 +284,7 @@ mod tests { assert_eq!(x, Nonce::from_commitment(&commitment[..]).unwrap()); commitment[0] = 42; assert!(Nonce::from_commitment(&commitment[..]).is_err()); + assert_eq!(encoding::encode_to_vec(&x), NONCE_COMMITMENT1); } #[cfg(feature = "serde")] diff --git a/src/confidential/nonce.rs b/src/confidential/nonce.rs index 0d9ca000..12310e0c 100644 --- a/src/confidential/nonce.rs +++ b/src/confidential/nonce.rs @@ -10,7 +10,9 @@ use secp256k1_zkp::{self, PublicKey, Secp256k1, SecretKey, Signing}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::hashes::sha256d; type ExplicitInner = [u8; 32]; @@ -18,6 +20,7 @@ type ConfInner = PublicKey; const EXPLICIT_LEN: usize = 32; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x02; const CONF_PREFIX_2: u8 = 0x03; @@ -247,3 +250,112 @@ impl<'de> Deserialize<'de> for Nonce { d.deserialize_seq(CommitVisitor) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Nonce`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Nonce { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit32(Some(1), id), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Nonce`] type. + pub struct Decoder(enum DecoderInner { + Done(Nonce), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Nonce::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Nonce::Explicit(bytes))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Nonce::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Nonce`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(bitcoin::secp256k1::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/confidential/value.rs b/src/confidential/value.rs index a8267148..930e20c3 100644 --- a/src/confidential/value.rs +++ b/src/confidential/value.rs @@ -14,8 +14,10 @@ use secp256k1_zkp::{ #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use super::CommitmentEncoder; use crate::confidential::AssetBlindingFactor; use crate::encode::{self, Decodable, Encodable}; +use crate::encoding; use crate::issuance::AssetId; type ExplicitInner = u64; @@ -23,6 +25,7 @@ type ConfInner = PedersenCommitment; const EXPLICIT_LEN: usize = 8; const CONFIDENTIAL_LEN: usize = 33; +const CONFIDENTIAL_LEN_LESS_PREFIX: usize = CONFIDENTIAL_LEN - 1; const CONF_PREFIX_1: u8 = 0x08; const CONF_PREFIX_2: u8 = 0x09; @@ -407,3 +410,112 @@ impl<'de> Deserialize<'de> for BlindingFactor { } } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`Asset`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(CommitmentEncoder<'e>); +} + +impl encoding::Encode for Value { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(match *self { + Self::Null => CommitmentEncoder::Null(0), + Self::Explicit(ref id) => CommitmentEncoder::Explicit8(Some(1), id.to_be_bytes()), + Self::Confidential(ref gen) => CommitmentEncoder::Explicit33(gen.serialize()), + }) + } +} + +decoder_state_machine! { + /// A decoder for the [`Value`] type. + pub struct Decoder(enum DecoderInner { + Done(Value), + Errored, + DecodePrefix { + decoder: encoding::ArrayDecoder<1>, + => transition_decode_prefix(prefix, ...) -> Result { + match prefix { + [0] => Ok(DecoderInner::Done(Value::Null)), + [1] => { + Ok(DecoderInner::DecodeExplicit { decoder: encoding::ArrayDecoder::default() }) + }, + [prefix @ (CONF_PREFIX_1 | CONF_PREFIX_2)] => { + Ok(DecoderInner::DecodeConfidential { decoder: encoding::ArrayDecoder::default(), prefix }) + }, + [prefix] => Err(DecoderErrorInner::InvalidConfidentialPrefix { prefix }) + } + } + }, + DecodeExplicit { + decoder: encoding::ArrayDecoder + => transition_decode_explicit(bytes, ...) -> Result { + Ok(DecoderInner::Done(Value::Explicit(u64::from_be_bytes(bytes)))) + } + }, + DecodeConfidential { + decoder: encoding::ArrayDecoder, + prefix: u8 + => transition_decode_confidential(x_coord, ...) -> Result { + let mut bytes = [0; CONFIDENTIAL_LEN]; + bytes[0] = prefix; + bytes[1..].copy_from_slice(&x_coord); + let gen = ConfInner::from_slice(&bytes) + .map_err(DecoderErrorInner::InvalidCommitment)?; + Ok(DecoderInner::Done(Value::Confidential(gen))) + } + }, + }); + + /// A decoder error for the [`Value`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + [macro-inserted decoder variants] + /// Confidential prefix was not one of the two allowable values. + InvalidConfidentialPrefix { + prefix: u8, + }, + /// Malformed confidential commitment. + InvalidCommitment(secp256k1_zkp::Error), + }); +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(_) => f.write_str("failed to decode prefix"), + Inner::DecodeExplicit(_) => f.write_str("failed to decode explicit value"), + Inner::DecodeConfidential(_) => f.write_str("failed to decode confidential value"), + Inner::InvalidConfidentialPrefix { prefix, .. } => { + write!( + f, + "confidential prefix 0x{:02x} was not one of 0, 1, 0x{:02x} or 0x{:02x}", + prefix, CONF_PREFIX_1, CONF_PREFIX_2, + ) + } + Inner::InvalidCommitment(_) => f.write_str("failed to parse confidential commitment"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::DecodePrefix(ref e) => Some(e), + Inner::DecodeExplicit(ref e) => Some(e), + Inner::DecodeConfidential(ref e) => Some(e), + Inner::InvalidConfidentialPrefix { .. } => None, + Inner::InvalidCommitment(ref e) => Some(e), + } + } +} + +impl Default for Decoder { + fn default() -> Self { + Self(DecoderInner::DecodePrefix { decoder: encoding::ArrayDecoder::default() }) + } +} diff --git a/src/internal_macros.rs b/src/internal_macros.rs index 3e2b7431..b9cf0c19 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -527,3 +527,261 @@ macro_rules! hex_deserialize( macro_rules! hex_script( ($e:expr) => (crate::Script::from_hex_no_prefix($e).expect("hex decoding")) ); + +/// Generates a state machine decoder satisfying the [`crate::encoding::Decoder`] trait. +/// +/// This macro creates a decoder that processes input bytes through multiple states, +/// transitioning between states based on decoded data. It automatically generates +/// both the decoder struct and associated error types. +/// +/// # Syntax +/// +/// ```ignore +/// decoder_state_machine! { +/// /// Documentation for the decoder struct +/// pub struct DecoderName(enum InnerEnumName { +/// Done(TargetType), +/// Errored, +/// StateName { +/// decoder: DecoderType, +/// field1: FieldType1, +/// field2: FieldType2, +/// => transition_function_name(output, ...) -> Result { +/// // Transition logic that returns Ok(NextState) or Err(error) +/// } +/// }, +/// // ... more states +/// }); +/// +/// /// Documentation for the error struct +/// pub struct ErrorName(enum InnerErrorName { +/// [macro-inserted decoder variants] +/// CustomError1(ErrorType1), +/// CustomError2 { field: ErrorType2 }, +/// // ... custom error variants +/// }); +/// } +/// ``` +/// +/// # Generated Code +/// +/// The macro generates: +/// - A public decoder struct wrapping a private enum +/// - A private enum with states, Done, and Errored variants +/// - Transition functions for each state +/// - A public error struct wrapping a private error enum +/// - A private error enum with decoder errors and custom variants +/// - `Decoder` trait implementation with `push_bytes`, `end`, and `read_limit` methods +/// - `Decode` impl for the target type +/// +/// The macro does **not** generate a `fmt::Display` or `std::error::Error` impl +/// for `ErrorName`, so the user must implement these outside of the macro. +/// +/// The variants that the macro inserts into `ErrorName` have the same names +/// as the variants on `InnerEnumName`, and they contain a single value of +/// type `::Error`. +/// +/// # State Structure +/// +/// Each state must have: +/// - `decoder: SomeDecoderType` - The decoder for this state's data +/// - Optional additional fields to carry state between transitions +/// - A transition function that processes the decoder's output +/// +/// # Transition Functions +/// +/// Transition functions: +/// - Take the decoder's output as first parameter +/// - Take any additional state fields as subsequent parameters +/// - Return `Result` +/// - Are called automatically when the decoder completes +/// +/// # Error Handling +/// +/// The macro automatically: +/// - Creates error variants for each decoder type +/// - Maps decoder errors to the appropriate variant +/// +/// # Example Usage +/// +/// ```ignore +/// decoder_state_machine! { +/// /// Decodes a length-prefixed string +/// pub struct StringDecoder(enum StringDecoderInner { +/// Done(String), +/// Errored, +/// ReadLength { +/// decoder: encoding::ArrayDecoder<4> +/// => transition_read_length(length_bytes, ...) -> Result { +/// let length = u32::from_be_bytes(length_bytes) as usize; +/// if length > MAX_STRING_LENGTH { +/// return Err(StringDecoderErrorInner::StringTooLong { length }); +/// } +/// Ok(StringDecoderInner::ReadData { +/// decoder: encoding::VecDecoder::new(length), +/// expected_length: length +/// }) +/// } +/// }, +/// ReadData { +/// decoder: encoding::VecDecoder, +/// expected_length: usize +/// => transition_read_data(data, expected_length, ...) -> Result { +/// if data.len() != expected_length { +/// return Err(StringDecoderErrorInner::LengthMismatch); +/// } +/// let string = String::from_utf8(data) +/// .map_err(StringDecoderErrorInner::InvalidUtf8)?; +/// Ok(StringDecoderInner::Done(string)) +/// } +/// }, +/// }); +/// +/// /// Errors that can occur during string decoding +/// #[derive(Debug)] +/// pub struct StringDecoderError(enum StringDecoderErrorInner { +/// [macro-inserted decoder variants] +/// StringTooLong { length: usize }, +/// LengthMismatch, +/// InvalidUtf8(std::string::FromUtf8Error), +/// }); +/// } +/// ``` +macro_rules! decoder_state_machine { + ( + $(#[$($struct_attr:tt)*])* + pub struct $outer_ty:ident(enum $inner_ty:ident { + Done($target_ty:ty), + Errored, + $( + $variant:ident { + decoder: $decoder_ty:ty$(,)? + $(, $field:ident: $field_ty:ty)* + => $transition_fn:ident($output:ident, ...) -> Result { + $($transition_fn_inner:tt)* + } + }, + )* + }); + + $(#[$($error_struct_attr:tt)*])* + pub struct $error_ty:ident(enum $inner_error_ty:ident { + [macro-inserted decoder variants] + $($extra_variants:tt)* + }); + ) => { + $(#[$($struct_attr)*])* + pub struct $outer_ty($inner_ty); + + #[allow(clippy::large_enum_variant)] + enum $inner_ty { + $($variant { + decoder: $decoder_ty + $(, $field: $field_ty)* + },)* + Done($target_ty), + Errored, + } + + impl $inner_ty { + $( + #[inline] + #[allow(clippy::unnecessary_wraps)] // returns a Result even if it never returns Err + fn $transition_fn( + $output: <$decoder_ty as $crate::encoding::Decoder>::Output + $(, $field: $field_ty)* + ) -> Result { + $($transition_fn_inner)* + } + )* + } + + $(#[$($error_struct_attr)*])* + pub struct $error_ty($inner_error_ty); + + $(#[$($error_struct_attr)*])* + enum $inner_error_ty { + $( + $variant(<$decoder_ty as $crate::encoding::Decoder>::Error), + )* + $($extra_variants)* + } + + impl $crate::encoding::Decoder for $outer_ty { + type Output = $target_ty; + type Error = $error_ty; + + fn push_bytes( + &mut self, + bytes: &mut &[u8], + ) -> Result<$crate::encoding::DecoderStatus, Self::Error> { + use $inner_ty as Inner; + loop { + match core::mem::replace(&mut self.0, Inner::Errored) { + $( + Inner::$variant { mut decoder $(, $field)* } => { + if decoder + .push_bytes(bytes) + .map_err($inner_error_ty::$variant) + .map_err($error_ty)? + .needs_more() + { + self.0 = Inner::$variant { decoder $(, $field)* }; + return Ok($crate::encoding::DecoderStatus::NeedsMore); + } + + let output = decoder.end() + .map_err($inner_error_ty::$variant) + .map_err($error_ty)?; + self.0 = $inner_ty::$transition_fn(output $(, $field)*) + .map_err($error_ty)?; + } + )* + Inner::Done(out) => { + self.0 = Inner::Done(out); + return Ok($crate::encoding::DecoderStatus::Ready); + } + Inner::Errored => panic!("called push_bytes() on an errored decoder"), + + } + } + } + + fn end(self) -> Result { + use $inner_ty as Inner; + match self.0 { + $( + Inner::$variant { decoder, .. } => { + decoder.end() + .map_err($inner_error_ty::$variant) + .map_err($error_ty)?; + // This unreachable! can be hit by badly behaved decoders where push_bytes() + // returns DecoderStatus::NeedMore, but end() returns Ok. In general, end() + // should return Ok only if push_bytes has returned DecoderStatus::Ready. + unreachable!( + "end() succeeded on decoder for {} state, but we did not leave that state", + stringify!($variant), + ) + } + )* + Inner::Done(out) => Ok(out), + Inner::Errored => panic!("called end() on an errored decoder"), + } + } + + fn read_limit(&self) -> usize { + use $inner_ty as Inner; + match self.0 { + $( + Inner::$variant { ref decoder, .. } => decoder.read_limit(), + )* + Inner::Done(_) | Inner::Errored => 0, + } + } + } + + impl $crate::encoding::Decode for $target_ty { + type Decoder = $outer_ty; + } + }; +} From c7e16cdc69eee344d37d3cdf29a5497baec343ab Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 15:45:36 +0000 Subject: [PATCH 04/14] confidential: implement Encode and Decode for rangeproof/surjectionproof Introduces the decoder_newtype! macro. As with the other macro, this one was hand-written based on multiple examples of hand-written code, but I let Claude write the doccomment (which I made some minor touchups to). There is a similarly-named macro in rust-bitcoin `primitives` but this is *not* that macro. A later PR will elaborate this macro, but this is all we need for now. --- src/confidential/mod.rs | 65 ++++++++++- src/confidential/range_proof.rs | 56 +++++++++- src/confidential/surjection_proof.rs | 56 +++++++++- src/internal_macros.rs | 155 +++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 4 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 335a179d..22fe9542 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -36,8 +36,14 @@ pub use self::asset::{ pub use self::nonce::{ Decoder as NonceDecoder, DecoderError as NonceDecoderError, Encoder as NonceEncoder, Nonce, }; -pub use self::range_proof::RangeProof; -pub use self::surjection_proof::SurjectionProof; +pub use self::range_proof::{ + Decoder as RangeProofDecoder, DecoderError as RangeProofDecoderError, + Encoder as RangeProofEncoder, RangeProof, +}; +pub use self::surjection_proof::{ + Decoder as SurjectionProofDecoder, DecoderError as SurjectionProofDecoderError, + Encoder as SurjectionProofEncoder, SurjectionProof, +}; pub use self::value::{ BlindingFactor as ValueBlindingFactor, Decoder as ValueDecoder, DecoderError as ValueDecoderError, Encoder as ValueEncoder, Value, @@ -88,6 +94,52 @@ impl encoding::ExactSizeEncoder for CommitmentEncoder<'_> { } } +/// Because the rust-secp256k1-zkp proof types have no `as_bytes()` method, we need +/// to serialize them to a byte vector before encoding them. +/// +/// This encoder accomplishes that -- this situation never happens in rust-bitcoin +/// so there is no "owned bytes encoder" shipped with bitcoin-consensus-encoding. +#[derive(Clone, Debug)] +struct PrefixedByteVecEncoder { + prefix_encoder: Option, + data: Vec, +} + +impl PrefixedByteVecEncoder { + pub fn new(data: Vec) -> Self { + Self { prefix_encoder: Some(encoding::CompactSizeEncoder::new(data.len())), data } + } +} + +impl encoding::Encoder for PrefixedByteVecEncoder { + fn current_chunk(&self) -> &[u8] { + if let Some(ref enc) = self.prefix_encoder { + return enc.current_chunk(); + } + &self.data + } + + fn advance(&mut self) -> encoding::EncoderStatus { + if let Some(ref mut enc) = self.prefix_encoder { + if enc.advance().has_finished() { + self.prefix_encoder = None; + if self.data.is_empty() { + return encoding::EncoderStatus::Finished; + } + } + encoding::EncoderStatus::HasMore + } else { + encoding::EncoderStatus::Finished + } + } +} + +impl encoding::ExactSizeEncoder for PrefixedByteVecEncoder { + fn len(&self) -> usize { + self.prefix_encoder.as_ref().map_or(0, encoding::CompactSizeEncoder::len) + self.data.len() + } +} + /// Error decoding hexadecimal string into tweak-like value. #[derive(Debug, Clone, PartialEq, Eq)] pub enum TweakHexDecodeError { @@ -191,6 +243,15 @@ mod tests { 1, 1, 1, 1, ]; + #[test] + fn prefixed_byte_encoder() { + assert_eq!(encoding::drain_to_vec(&mut PrefixedByteVecEncoder::new(vec![])), [0]); + assert_eq!( + encoding::drain_to_vec(&mut PrefixedByteVecEncoder::new(vec![1, 2, 3])), + [3, 1, 2, 3] + ); + } + #[test] fn encode_length() { let val_encodings = [ diff --git a/src/confidential/range_proof.rs b/src/confidential/range_proof.rs index 89894333..441b8c4f 100644 --- a/src/confidential/range_proof.rs +++ b/src/confidential/range_proof.rs @@ -3,6 +3,7 @@ //! Range Proofs use core::convert::TryInto; +use core::fmt; use std::io; use secp256k1_zkp::rand::{CryptoRng, RngCore}; @@ -11,7 +12,7 @@ use secp256k1_zkp::{self, Generator, PedersenCommitment, Secp256k1, SecretKey, S use serde::{Deserializer, Serializer}; use crate::confidential::ValueBlindingFactor; -use crate::encode; +use crate::{encode, encoding}; /// A range proof, which represents a proof that a confidential value lies within /// some range (typically `[0, 2^64)`). @@ -190,3 +191,56 @@ impl<'de> serde::Deserialize<'de> for RangeProof { .map(|inner| Self { inner: inner.map(Box::new) }) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`RangeProof`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(super::PrefixedByteVecEncoder); +} + +impl encoding::Encode for RangeProof { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(super::PrefixedByteVecEncoder::new(self.to_vec())) + } +} + +decoder_newtype! { + /// Decoder for the [`RangeProof`] type. + #[derive(Default)] + pub struct Decoder(encoding::ByteVecDecoder); + + /// Decoder error for the [`RangeProof`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + Decode(encoding::ByteVecDecoderError), + RangeProof(secp256k1_zkp::Error), + }); + + impl Decode for RangeProof { + fn convert_inner(v) -> Result<_, DecoderErrorInner> { + Self::Output::from_slice(&v).map_err(DecoderErrorInner::RangeProof) + } + } +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(..) => f.write_str("error decoding byte vector"), + Inner::RangeProof(..) => f.write_str("error decoding range proof"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(ref e) => Some(e), + Inner::RangeProof(ref e) => Some(e), + } + } +} diff --git a/src/confidential/surjection_proof.rs b/src/confidential/surjection_proof.rs index 6fb6f8ea..9a9ceed5 100644 --- a/src/confidential/surjection_proof.rs +++ b/src/confidential/surjection_proof.rs @@ -2,6 +2,7 @@ //! Surjection Proofs +use core::fmt; use std::io; use secp256k1_zkp::rand::{CryptoRng, RngCore}; @@ -10,7 +11,7 @@ use secp256k1_zkp::{self, Generator, Secp256k1, Signing, Tweak, ZERO_TWEAK}; use serde::{Deserializer, Serializer}; use crate::confidential::{AssetBlindingFactor, AssetId}; -use crate::encode; +use crate::{encode, encoding}; /// A surjection proof, proving that an asset commitment commits to the same asset ID /// as a commitment from a given set. @@ -143,3 +144,56 @@ impl<'de> serde::Deserialize<'de> for SurjectionProof { .map(|inner| Self { inner: inner.map(Box::new) }) } } + +encoding::encoder_newtype_exact! { + /// Encoder for the [`SurjectionProof`] type. + #[derive(Clone, Debug)] + pub struct Encoder<'e>(super::PrefixedByteVecEncoder); +} + +impl encoding::Encode for SurjectionProof { + type Encoder<'e> = Encoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + Encoder::new(super::PrefixedByteVecEncoder::new(self.to_vec())) + } +} + +decoder_newtype! { + /// Decoder for the [`SurjectionProof`] type. + #[derive(Default)] + pub struct Decoder(encoding::ByteVecDecoder); + + /// Decoder error for the [`SurjectionProof`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct DecoderError(enum DecoderErrorInner { + Decode(encoding::ByteVecDecoderError), + SurjectionProof(secp256k1_zkp::Error), + }); + + impl Decode for SurjectionProof { + fn convert_inner(v) -> Result<_, DecoderErrorInner> { + Self::Output::from_slice(&v).map_err(DecoderErrorInner::SurjectionProof) + } + } +} + +impl fmt::Display for DecoderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(..) => f.write_str("error decoding byte vector"), + Inner::SurjectionProof(..) => f.write_str("error decoding surjection proof"), + } + } +} + +impl std::error::Error for DecoderError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use DecoderErrorInner as Inner; + match self.0 { + Inner::Decode(ref e) => Some(e), + Inner::SurjectionProof(ref e) => Some(e), + } + } +} diff --git a/src/internal_macros.rs b/src/internal_macros.rs index b9cf0c19..a20a8193 100644 --- a/src/internal_macros.rs +++ b/src/internal_macros.rs @@ -785,3 +785,158 @@ macro_rules! decoder_state_machine { } }; } + +/// Generates a simple decoder wrapper that converts output from an inner decoder. +/// +/// This macro creates a decoder that wraps an existing decoder and applies a conversion +/// function to transform the inner decoder's output into the target type. It's simpler +/// than `decoder_state_machine!` as it only wraps a single decoder without state transitions. +/// +/// # Syntax +/// +/// ```ignore +/// decoder_newtype! { +/// /// Documentation for the decoder struct +/// pub struct DecoderName(InnerDecoderType); +/// +/// /// Documentation for the error struct +/// pub struct ErrorName(enum InnerErrorName { +/// // The first variant must be called Decode and hold the inner decoder type. +/// Decode(InnerDecoderErrorType), +/// // All other variants are free-form. +/// CustomError1(ErrorType1), +/// CustomError2 { field: ErrorType2 }, +/// // ... custom error variants +/// }); +/// +/// impl Decode for TargetType { +/// fn convert_inner(output_var) -> Result<_, ErrorName> { +/// // Conversion logic +/// } +/// } +/// } +/// ``` +/// +/// # Generated Code +/// +/// The macro generates: +/// - A public decoder struct wrapping the inner decoder +/// - A public error struct wrapping a private error enum +/// - A private error enum with `Decode` variant and custom variants +/// - `Decoder` trait implementation with `push_bytes`, `end`, and `read_limit` methods +/// - `Decode` trait implementation for the target type +/// +/// The macro does **not** generate a `fmt::Display` or `std::error::Error` impl +/// for `ErrorName`, so the user must implement these outside of the macro. +/// +/// # Parameters +/// +/// - `DecoderName` - The wrapper decoder struct name +/// - `InnerDecoderType` - The existing decoder type to wrap +/// - `ErrorName` - The error struct name +/// - `InnerErrorName` - The private error enum name +/// - `InnerDecoderErrorType` - The error type from the inner decoder +/// - `TargetType` - The final output type after conversion +/// - `convert_inner` - Function that converts inner output to target type +/// +/// # Conversion Function +/// +/// The conversion function: +/// - Takes the inner decoder's output as its parameter +/// - Returns `Result` +/// - Is called automatically when the inner decoder completes +/// - Can return custom errors defined in the error enum +/// +/// # Error Handling +/// +/// The macro automatically: +/// - Creates a `Decode` variant containing the inner decoder's error +/// - Maps inner decoder errors to the `Decode` variant +/// - Allows custom error variants for conversion failures +/// +/// # Example Usage +/// +/// ```ignore +/// decoder_newtype! { +/// /// Decoder for range proofs +/// #[derive(Default)] +/// pub struct Decoder(encoding::ByteVecDecoder); +/// +/// /// Decoder error for range proofs +/// #[derive(Clone, PartialEq, Eq, Debug)] +/// pub struct DecoderError(enum DecoderErrorInner { +/// Decode(encoding::ByteVecDecoderError), +/// RangeProof(secp256k1_zkp::Error), +/// }); +/// +/// impl Decode for RangeProof { +/// fn convert_inner(v) -> Result<_, DecoderError> { +/// RangeProof::from_slice(&v).map_err(DecoderError::RangeProof) +/// } +/// } +/// } +/// ``` +macro_rules! decoder_newtype { + ( + $(#[$($struct_attr:tt)*])* + pub struct $outer_ty:ident($inner_ty:ty); + + $(#[$($error_struct_attr:tt)*])* + pub struct $error_ty:ident(enum $inner_error_ty:ident { + Decode($inner_ty_error:ty), + $($extra_variants:tt)* + }); + + impl Decode for $target_ty:ty { + fn convert_inner($output:ident) -> Result<_, $error_inner1:ty> { + $($output_fn_inner:tt)* + } + } + ) => { + $(#[$($struct_attr)*])* + pub struct $outer_ty($inner_ty); + + $(#[$($error_struct_attr)*])* + pub struct $error_ty($inner_error_ty); + + $(#[$($error_struct_attr)*])* + enum $inner_error_ty { + Decode($inner_ty_error), + $($extra_variants)* + } + + impl $crate::encoding::Decoder for $outer_ty { + type Output = $target_ty; + type Error = $error_ty; + + fn push_bytes( + &mut self, + bytes: &mut &[u8], + ) -> Result<$crate::encoding::DecoderStatus, Self::Error> { + self.0 + .push_bytes(bytes) + .map_err($inner_error_ty::Decode) + .map_err($error_ty) + } + + fn end(self) -> Result { + let $output = self.0 + .end() + .map_err($inner_error_ty::Decode) + .map_err($error_ty)?; + let converted = { + $($output_fn_inner)* + }; + converted.map_err($error_ty) + } + + fn read_limit(&self) -> usize { + self.0.read_limit() + } + } + + impl $crate::encoding::Decode for $target_ty{ + type Decoder = $outer_ty; + } + }; +} From c13d392a530ce7a0356f8bcf64c1bf292b1b0500 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Thu, 25 Jun 2026 19:06:38 +0000 Subject: [PATCH 05/14] rename transaction to transaction/mod.rs --- src/{transaction.rs => transaction/mod.rs} | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) rename src/{transaction.rs => transaction/mod.rs} (99%) diff --git a/src/transaction.rs b/src/transaction/mod.rs similarity index 99% rename from src/transaction.rs rename to src/transaction/mod.rs index 694c43ac..f68af9ba 100644 --- a/src/transaction.rs +++ b/src/transaction/mod.rs @@ -2406,7 +2406,7 @@ mod tests { #[test] fn discount_vsize() { - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_pegin.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_pegin.hex")); assert_eq!(tx.input.len(), 1); assert!(tx.input[0].is_pegin()); assert_eq!(tx.output.len(), 2); @@ -2415,7 +2415,7 @@ mod tests { assert_eq!(tx.discount_weight(), 2403); assert_eq!(tx.discount_vsize(), 601); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_tx.hex")); assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 2); assert_eq!(tx.weight(), 5330); @@ -2423,7 +2423,7 @@ mod tests { assert_eq!(tx.discount_weight(), 863); assert_eq!(tx.discount_vsize(), 216); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in3out_tx.hex")); assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10107); @@ -2431,7 +2431,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1173); assert_eq!(tx.discount_vsize(), 294); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_exp.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_exp.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 1302); @@ -2439,7 +2439,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10300); @@ -2447,7 +2447,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10300); @@ -2455,7 +2455,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1302); assert_eq!(tx.discount_vsize(), 326); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in3out_tx2.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in3out_tx2.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10536); @@ -2463,7 +2463,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1538); assert_eq!(tx.discount_vsize(), 385); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/3in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/3in3out_tx.hex")); assert_eq!(tx.input.len(), 3); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 10922); @@ -2471,7 +2471,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1860); assert_eq!(tx.discount_vsize(), 465); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/4in3out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/4in3out_tx.hex")); assert_eq!(tx.input.len(), 4); assert_eq!(tx.output.len(), 3); assert_eq!(tx.weight(), 11192); @@ -2479,7 +2479,7 @@ mod tests { assert_eq!(tx.discount_weight(), 2130); assert_eq!(tx.discount_vsize(), 533); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in4out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in4out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 4); assert_eq!(tx.weight(), 15261); @@ -2487,7 +2487,7 @@ mod tests { assert_eq!(tx.discount_weight(), 1764); assert_eq!(tx.discount_vsize(), 441); - let tx: Transaction = hex_deserialize!(include_str!("../tests/data/2in5out_tx.hex")); + let tx: Transaction = hex_deserialize!(include_str!("../../tests/data/2in5out_tx.hex")); assert_eq!(tx.input.len(), 2); assert_eq!(tx.output.len(), 5); assert_eq!(tx.weight(), 20030); @@ -2501,8 +2501,8 @@ mod tests { // Check that rust-elements can deserialize tests vector from ELIP203 // from // https://github.com/ElementsProject/ELIPs/blob/main/elip-0203.mediawiki - let tx3: Transaction = hex_deserialize!(include_str!("../tests/data/elip203_3.hex")); - let tx4: Transaction = hex_deserialize!(include_str!("../tests/data/elip203_4.hex")); + let tx3: Transaction = hex_deserialize!(include_str!("../../tests/data/elip203_3.hex")); + let tx4: Transaction = hex_deserialize!(include_str!("../../tests/data/elip203_4.hex")); let max_money = 2_100_000_000_000_000; assert!(tx3.input[0].asset_issuance.amount.explicit().unwrap() > max_money); assert!(tx4.input[0].asset_issuance.inflation_keys.explicit().unwrap() > max_money); @@ -2513,7 +2513,7 @@ mod tests { use crate::encode::{serialize, deserialize}; // Start with a transaction that has a pegin. - let base_tx: Transaction = hex_deserialize!(include_str!("../tests/data/1in2out_pegin.hex")); + let base_tx: Transaction = hex_deserialize!(include_str!("../../tests/data/1in2out_pegin.hex")); // Test case (a): input witnesses but no output witnesses let mut tx_input_only = base_tx.clone(); From 9f82b4b32707e2193ebd54c894cab5bb3f330b77 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Fri, 26 Jun 2026 13:55:14 +0000 Subject: [PATCH 06/14] serde: remove impls for PSET and Transaction Leave the impl for BlockHeader (which is used by the functionary in a couple of places) (though I'd also like to delete this), the impls for the hash types (which are manually implemented and have regression tests), and the confidential stuff (which are also manually implemented and fairly straightforward). But for Transaction and PSET these impls are untenable. They are: * based on serde-derives (though 'manually' through the serde_struct_impl macro) of extremely complicated and fragile types, exposing implementation details and probably bypassing invariants on deserialization * not specified or documented anywhere * have very poor error messages on deserialization * not tested anywhere (not a single test broke with this change!!) If people are depending on these, we should provide some sort of compat crate/module. We can wait until they are trying to upgrade rust-elements and file complaints to do this. I'm skeptical that anybody *is*, given the above problems, at least not on purpose. If people want serde impls but don't need backward compatibility, we can provide that via the bitcoin-consensus-encoding crate, which will have general-purpose "serde-encode consensus objects in hex/bytes" adaptors in an upcoming version. --- src/block.rs | 1 - src/lib.rs | 2 - src/locktime.rs | 3 - src/opcodes.rs | 12 -- src/pset/map/global.rs | 11 -- src/pset/map/input.rs | 33 ----- src/pset/map/output.rs | 12 -- src/pset/mod.rs | 1 - src/pset/raw.rs | 16 --- src/serde_utils.rs | 276 ----------------------------------------- src/transaction/mod.rs | 7 -- 11 files changed, 374 deletions(-) delete mode 100644 src/serde_utils.rs diff --git a/src/block.rs b/src/block.rs index f022f1ae..c2153b33 100644 --- a/src/block.rs +++ b/src/block.rs @@ -360,7 +360,6 @@ pub struct Block { /// Complete list of transaction in the block pub txdata: Vec, } -serde_struct_impl!(Block, header, txdata); impl_consensus_encoding!(Block, header, txdata); impl Block { diff --git a/src/lib.rs b/src/lib.rs index 21543b1f..b36491e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,8 +69,6 @@ mod parse; pub mod pset; pub mod schnorr; pub mod script; -#[cfg(feature = "serde")] -mod serde_utils; pub mod sighash; pub mod taproot; mod transaction; diff --git a/src/locktime.rs b/src/locktime.rs index 0fd8c043..51c22f71 100644 --- a/src/locktime.rs +++ b/src/locktime.rs @@ -66,7 +66,6 @@ pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000; /// ``` #[allow(clippy::derive_ord_xor_partial_ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LockTime { /// A block height lock time value. /// @@ -292,7 +291,6 @@ impl Decodable for LockTime { /// An absolute block height, guaranteed to always contain a valid height value. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Height(u32); impl Height { @@ -377,7 +375,6 @@ impl TryFrom for Height { /// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x - /// threshold) seconds since epoch'. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Time(u32); impl Time { diff --git a/src/opcodes.rs b/src/opcodes.rs index fb8648c4..d9cfec5b 100644 --- a/src/opcodes.rs +++ b/src/opcodes.rs @@ -20,8 +20,6 @@ #![allow(non_camel_case_types)] -#[cfg(feature = "serde")] use serde; - use std::fmt; // Note: I am deliberately not implementing PartialOrd or Ord on the @@ -862,16 +860,6 @@ impl fmt::Display for All { } } -#[cfg(feature = "serde")] -impl serde::Serialize for All { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - /// Empty stack is also FALSE pub static OP_FALSE: All = all::OP_PUSHBYTES_0; /// Number 1 is also TRUE diff --git a/src/pset/map/global.rs b/src/pset/map/global.rs index 8051cc39..4ed267e3 100644 --- a/src/pset/map/global.rs +++ b/src/pset/map/global.rs @@ -58,7 +58,6 @@ const PSBT_ELEMENTS_GLOBAL_TX_MODIFIABLE: u8 = 0x01; /// Global transaction data #[derive(Debug, Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TxData { /// Transaction version. Must be 2. pub version: u32, @@ -93,10 +92,8 @@ impl Default for TxData { /// A key-value map for global data. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Global { /// Global transaction data - #[cfg_attr(feature = "serde", serde(flatten))] pub tx_data: TxData, /// The version number of this PSET. Must be present. pub version: u32, @@ -109,16 +106,8 @@ pub struct Global { /// Elements tx modifiable flag pub elements_tx_modifiable_flag: Option, /// Other Proprietary fields - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_as_seq_byte_values") - )] pub proprietary: BTreeMap>, /// Unknown global key-value pairs. - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_as_seq_byte_values") - )] pub unknown: BTreeMap>, } diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 53438a1a..b146041f 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -172,7 +172,6 @@ const PSBT_ELEMENTS_IN_BLINDED_ISSUANCE: u8 = 0x15; /// A key-value map for an input of the corresponding index in the unsigned /// transaction. #[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Input { /// The non-witness transaction this input spends from. Should only be /// [`std::option::Option::Some`] for inputs which spend non-segwit outputs or @@ -184,10 +183,6 @@ pub struct Input { pub witness_utxo: Option, /// A map from public keys to their corresponding signature as would be /// pushed to the stack from a scriptSig or witness. - #[cfg_attr( - feature = "serde", - serde(with = "crate::serde_utils::btreemap_byte_values") - )] pub partial_sigs: BTreeMap>, /// The sighash type to be used for this input. Signatures for this input /// must use the sighash type. @@ -198,7 +193,6 @@ pub struct Input { pub witness_script: Option