From f3094b5dd0bb390eb1f38d2c75f53b6f5128ebb9 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 15 Jul 2026 19:59:01 +0000 Subject: [PATCH 1/4] transaction: use AssetEntropy in AssetIssuance entropy field Somehow I neglected to do this in https://github.com/ElementsProject/rust-elements/pull/278 --- src/genesis.rs | 4 ++-- src/internal_macros.rs | 8 ++++---- src/issuance.rs | 22 ++++++++++++++++++++++ src/pset/map/input.rs | 20 ++++++++++++-------- src/pset/serialize.rs | 3 ++- src/transaction/decoders.rs | 6 +++--- src/transaction/encoders.rs | 2 +- src/transaction/mod.rs | 16 +++++++++------- 8 files changed, 55 insertions(+), 26 deletions(-) diff --git a/src/genesis.rs b/src/genesis.rs index 4aed10ae..112f90fe 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -20,7 +20,7 @@ use crate::hashes::{sha256, HashEngine}; use crate::opcodes::all::OP_RETURN; use crate::opcodes::OP_TRUE; use crate::{confidential, script, AssetId, Block, BlockExtData, BlockHash, BlockHeader, LockTime, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness}; -use crate::{AssetIssuance, ContractHash, OutPoint, Txid}; +use crate::{AssetEntropy, AssetIssuance, ContractHash, OutPoint, Txid}; use crate::confidential::Nonce; /// Parameters that influence chain consensus. The contents of the genesis block for a given network @@ -147,7 +147,7 @@ fn liquid_genesis_asset_tx(network_params: &NetworkParams) -> Option Self { + pub const fn from_byte_array(inner: [u8; 32]) -> Self { Self(inner) } /// The raw bytes within the wrapper type. - pub fn as_byte_array(&self) -> &[u8; 32] { + pub const fn as_byte_array(&self) -> &[u8; 32] { &self.0 } /// The raw bytes within the wrapper type. - pub fn to_byte_array(self) -> [u8; 32] { + pub const fn to_byte_array(self) -> [u8; 32] { self.0 } /// (Private) convert a sha256 midstate to an object. - fn from_midstate(value: crate::hashes::sha256::Midstate) -> Self { + const fn from_midstate(value: crate::hashes::sha256::Midstate) -> Self { Self(value.to_parts().0) } } diff --git a/src/issuance.rs b/src/issuance.rs index 82e31e26..9392b759 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -51,6 +51,28 @@ impl_sha256_midstate_wrapper! { pub struct AssetEntropy([u8; 32]); } +impl AssetEntropy { + /// The all-zeroes "entropy" used for new issuances (vs reissuances). + pub const NEW_ISSUANCE: Self = Self([0; 32]); + + /// Re-interpret the asset entropy as a contract hash. + pub fn into_contract_hash(self) -> ContractHash { + ContractHash::from_byte_array(self.0) + } +} + +impl Encodable for AssetEntropy { + fn consensus_encode(&self, e: W) -> Result { + self.0.consensus_encode(e) + } +} + +impl Decodable for AssetEntropy { + fn consensus_decode(d: D) -> Result { + <[u8; 32]>::consensus_decode(d).map(Self) + } +} + impl_sha256_midstate_wrapper! { /// An issued asset ID. pub struct AssetId([u8; 32]); diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 1aa02d75..98ec6734 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -21,7 +21,7 @@ use std::{ }; use crate::taproot::{ControlBlock, LeafVersion, TapNodeHash, TapLeafHash}; -use crate::{schnorr, AssetId, ContractHash}; +use crate::{schnorr, AssetId}; use crate::{confidential, locktime}; use crate::encode::{self, Decodable}; @@ -260,7 +260,7 @@ pub struct Input { /// Issuance blinding nonce pub issuance_blinding_nonce: Option, /// Issuance asset entropy - pub issuance_asset_entropy: Option<[u8; 32]>, + pub issuance_asset_entropy: Option, /// input utxo rangeproof pub in_utxo_rangeproof: Option, /// Proof that blinded issuance matches the commitment @@ -531,12 +531,14 @@ impl Input { txid: self.previous_txid, vout: self.previous_output_index, }; - let contract_hash = - ContractHash::from_byte_array(self.issuance_asset_entropy.unwrap_or_default()); + let contract_hash = self + .issuance_asset_entropy + .unwrap_or_default() + .into_contract_hash(); AssetId::generate_asset_entropy(prevout, contract_hash) } else { // re-issuance - AssetEntropy::from_byte_array(self.issuance_asset_entropy.unwrap_or_default()) + self.issuance_asset_entropy.unwrap_or_default() }; let asset_id = AssetId::from_entropy(entropy); let token_id = @@ -755,7 +757,7 @@ impl Map for Input { impl_pset_prop_insert_pair!(self.issuance_blinding_nonce <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_ASSET_ENTROPY => { - impl_pset_prop_insert_pair!(self.issuance_asset_entropy <= | ); + impl_pset_prop_insert_pair!(self.issuance_asset_entropy <= | ); } PSBT_ELEMENTS_IN_UTXO_RANGEPROOF => { impl_pset_prop_insert_pair!(self.in_utxo_rangeproof <= | ); @@ -1186,7 +1188,9 @@ mod tests { use crate::confidential; use crate::pset::PartiallySignedTransaction; - use crate::{AssetIssuance, LockTime, Transaction, TxIn, TxInWitness}; + use crate::{AssetEntropy, AssetIssuance, LockTime, Transaction, TxIn, TxInWitness}; + + const DUMMY_ENTROPY: AssetEntropy = AssetEntropy::from_byte_array([1; 32]); // See `pset::map::output::tests::from_tx_does_not_spuriously_set_proofs_on_unblinded_outputs`. // Same principle, but for the asset issuance rangeproofs in the input witnesses. @@ -1195,7 +1199,7 @@ mod tests { let txin = TxIn { asset_issuance: AssetIssuance { asset_blinding_nonce: ZERO_TWEAK, - asset_entropy: [1u8; 32], + asset_entropy: DUMMY_ENTROPY, amount: confidential::Value::Explicit(1000), inflation_keys: confidential::Value::Null, }, diff --git a/src/pset/serialize.rs b/src/pset/serialize.rs index 28b52e05..0911d3e7 100644 --- a/src/pset/serialize.rs +++ b/src/pset/serialize.rs @@ -24,7 +24,7 @@ use crate::encode::{ self, deserialize, deserialize_partial, serialize, Decodable, Encodable, VarInt, }; use crate::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; -use crate::{AssetId, BlockHash, RangeProof, Script, SurjectionProof, Transaction, TxOut, Txid}; +use crate::{AssetEntropy, AssetId, BlockHash, RangeProof, Script, SurjectionProof, Transaction, TxOut, Txid}; use bitcoin; use bitcoin::bip32::{ChildNumber, Fingerprint, KeySource}; use bitcoin::{key::XOnlyPublicKey, PublicKey}; @@ -59,6 +59,7 @@ pub fn serialize_hex(data: &T) -> String { impl_pset_de_serialize!(Transaction); impl_pset_de_serialize!(TxOut); +impl_pset_de_serialize!(AssetEntropy); impl_pset_de_serialize!(AssetId); impl_pset_de_serialize!(u8); impl_pset_de_serialize!(u32); diff --git a/src/transaction/decoders.rs b/src/transaction/decoders.rs index 9e34e931..d2ab85c5 100644 --- a/src/transaction/decoders.rs +++ b/src/transaction/decoders.rs @@ -8,8 +8,8 @@ use core::fmt; use super::{ - AssetIssuance, OutPoint, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, - Txid, + AssetEntropy, AssetIssuance, OutPoint, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, + TxOutWitness, Txid, }; use crate::confidential::{RangeProofDecoder, RangeProofDecoderError}; use crate::encoding::{ @@ -104,7 +104,7 @@ decoder_newtype! { asset_blinding_nonce: secp256k1_zkp::Tweak::from_inner(asset_blinding_nonce) .map_err(AssetIssuanceDecoderErrorInner::InvalidTweak) .map_err(AssetIssuanceDecoderError)?, - asset_entropy, + asset_entropy: AssetEntropy::from_byte_array(asset_entropy), amount, inflation_keys, }) diff --git a/src/transaction/encoders.rs b/src/transaction/encoders.rs index 17bcc0dd..72843f49 100644 --- a/src/transaction/encoders.rs +++ b/src/transaction/encoders.rs @@ -77,7 +77,7 @@ impl Encode for AssetIssuance { fn encoder(&self) -> Self::Encoder<'_> { AssetIssuanceEncoder::new(Encoder4::new( ArrayRefEncoder::without_length_prefix(self.asset_blinding_nonce.as_ref()), - ArrayRefEncoder::without_length_prefix(&self.asset_entropy), + ArrayRefEncoder::without_length_prefix(self.asset_entropy.as_byte_array()), self.amount.encoder(), self.inflation_keys.encoder(), )) diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 1f3c6847..e8b88f2b 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -28,7 +28,7 @@ use bitcoin::{self, VarInt}; use bitcoin::hashes::Hash as _; use crate::hashes::{sha256d, HashEngine as _}; -use crate::{confidential, ContractHash}; +use crate::confidential; use crate::encode::{self, Encodable, Decodable}; use crate::issuance::{AssetEntropy, AssetId}; use crate::opcodes; @@ -52,7 +52,7 @@ pub struct AssetIssuance { /// Zero for a new asset issuance; otherwise a blinding factor for the input pub asset_blinding_nonce: Tweak, /// Freeform entropy field - pub asset_entropy: [u8; 32], + pub asset_entropy: AssetEntropy, /// Amount of asset to issue pub amount: confidential::Value, /// Amount of inflation keys to issue @@ -64,7 +64,7 @@ impl AssetIssuance { pub fn null() -> Self { AssetIssuance { asset_blinding_nonce: ZERO_TWEAK, - asset_entropy: [0; 32], + asset_entropy: AssetEntropy::NEW_ISSUANCE, amount: confidential::Value::Null, inflation_keys: confidential::Value::Null, } @@ -549,12 +549,14 @@ impl TxIn { /// whether there is an issuance in this input. Returns (`asset_id`, `token_id`) pub fn issuance_ids(&self) -> (AssetId, AssetId) { let entropy = if self.asset_issuance.asset_blinding_nonce == ZERO_TWEAK { - let contract_hash = - ContractHash::from_byte_array(self.asset_issuance.asset_entropy); + let contract_hash = self + .asset_issuance + .asset_entropy + .into_contract_hash(); AssetId::generate_asset_entropy(self.previous_output, contract_hash) } else { // re-issuance - AssetEntropy::from_byte_array(self.asset_issuance.asset_entropy) + self.asset_issuance.asset_entropy }; let asset_id = AssetId::from_entropy(entropy); let token_id = @@ -2046,7 +2048,7 @@ mod tests { tx.input[0].asset_issuance, AssetIssuance { asset_blinding_nonce: ZERO_TWEAK, - asset_entropy: [0; 32], + asset_entropy: AssetEntropy::NEW_ISSUANCE, amount: confidential::Value::from_commitment( &[ 0x09, 0x81, 0x65, 0x4e, 0xb5, 0xcc, 0xd9, 0x92, 0x7b, 0x8b, 0xea, 0x94, 0x99, 0x7d, 0xce, 0x4a, From 426dfc0714d649d801b66561ba2d666438f573f8 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 15 Jul 2026 20:58:52 +0000 Subject: [PATCH 2/4] issuance: implement Encode and Decode for AssetEntropy --- src/issuance.rs | 32 ++++++++++++++++++++++++++++++++ src/lib.rs | 5 ++++- src/transaction/decoders.rs | 10 +++++----- src/transaction/encoders.rs | 4 ++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index 9392b759..f00f5e11 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -14,6 +14,7 @@ //! Asset Issuance +use core::fmt; use std::io; use crate::encode::{self, Encodable, Decodable}; @@ -73,6 +74,37 @@ impl Decodable for AssetEntropy { } } +encoding::encoder_newtype_exact! { + /// Encoder for the [`OutPoint`] type. + #[derive(Clone, Debug)] + pub struct AssetEntropyEncoder<'e>(encoding::ArrayRefEncoder<'e, 32>); +} + +impl encoding::Encode for AssetEntropy { + type Encoder<'e> = AssetEntropyEncoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + AssetEntropyEncoder::new(encoding::ArrayRefEncoder::without_length_prefix(&self.0)) + } +} + +decoder_newtype! { + /// Decoder for the [`AssetEntropy`] type. + #[derive(Default)] + pub struct AssetEntropyDecoder(encoding::ArrayDecoder<32>); + + /// Decoder error for the [`AssetEntropy`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct AssetEntropyDecoderError(encoding::UnexpectedEofError); + const ERROR_DISPLAY = "error decoding asset entropy"; + + impl Decode for AssetEntropy { + fn convert_inner(bytes) -> Result<_, UnexpectedEofError> { + Ok(AssetEntropy::from_byte_array(bytes)) + } + } +} + impl_sha256_midstate_wrapper! { /// An issued asset ID. pub struct AssetId([u8; 32]); diff --git a/src/lib.rs b/src/lib.rs index 68b2a1c7..87e8e864 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,10 @@ pub use crate::confidential::{RangeProof, SurjectionProof}; pub use crate::ext::{ReadExt, WriteExt}; pub use crate::fast_merkle_root::fast_merkle_root; pub use crate::hash_types::*; -pub use crate::issuance::{AssetEntropy, AssetId, ContractHash}; +pub use crate::issuance::{ + AssetEntropy, AssetEntropyDecoder, AssetEntropyDecoderError, AssetEntropyEncoder, AssetId, + ContractHash, +}; pub use crate::locktime::LockTime; pub use crate::schnorr::{SchnorrSig, SchnorrSigError}; pub use crate::script::Script; diff --git a/src/transaction/decoders.rs b/src/transaction/decoders.rs index d2ab85c5..1c9cf9c5 100644 --- a/src/transaction/decoders.rs +++ b/src/transaction/decoders.rs @@ -8,8 +8,8 @@ use core::fmt; use super::{ - AssetEntropy, AssetIssuance, OutPoint, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, - TxOutWitness, Txid, + AssetIssuance, OutPoint, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, + Txid, }; use crate::confidential::{RangeProofDecoder, RangeProofDecoderError}; use crate::encoding::{ @@ -81,7 +81,7 @@ decoder_newtype! { #[derive(Default)] pub struct AssetIssuanceDecoder(Decoder4< ArrayDecoder<32>, - ArrayDecoder<32>, + crate::AssetEntropyDecoder, crate::confidential::ValueDecoder, crate::confidential::ValueDecoder, >); @@ -90,7 +90,7 @@ decoder_newtype! { pub struct AssetIssuanceDecoderError(enum AssetIssuanceDecoderErrorInner { Decode(Decoder4Error< UnexpectedEofError, - UnexpectedEofError, + crate::AssetEntropyDecoderError, crate::confidential::ValueDecoderError, crate::confidential::ValueDecoderError, >), @@ -104,7 +104,7 @@ decoder_newtype! { asset_blinding_nonce: secp256k1_zkp::Tweak::from_inner(asset_blinding_nonce) .map_err(AssetIssuanceDecoderErrorInner::InvalidTweak) .map_err(AssetIssuanceDecoderError)?, - asset_entropy: AssetEntropy::from_byte_array(asset_entropy), + asset_entropy, amount, inflation_keys, }) diff --git a/src/transaction/encoders.rs b/src/transaction/encoders.rs index 72843f49..ec8e66a5 100644 --- a/src/transaction/encoders.rs +++ b/src/transaction/encoders.rs @@ -65,7 +65,7 @@ encoder_newtype_exact! { #[derive(Clone, Debug)] pub struct AssetIssuanceEncoder<'e>(Encoder4< ArrayRefEncoder<'e, 32>, - ArrayRefEncoder<'e, 32>, + crate::AssetEntropyEncoder<'e>, crate::confidential::ValueEncoder<'e>, crate::confidential::ValueEncoder<'e>, >); @@ -77,7 +77,7 @@ impl Encode for AssetIssuance { fn encoder(&self) -> Self::Encoder<'_> { AssetIssuanceEncoder::new(Encoder4::new( ArrayRefEncoder::without_length_prefix(self.asset_blinding_nonce.as_ref()), - ArrayRefEncoder::without_length_prefix(self.asset_entropy.as_byte_array()), + self.asset_entropy.encoder(), self.amount.encoder(), self.inflation_keys.encoder(), )) From c0818262bb6446ca44155e39f224eb3acbd286d7 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 15 Jul 2026 20:29:01 +0000 Subject: [PATCH 3/4] issuance: introduce AssetBlindingNonce newtype for issuance blinding nonce Previously we had been using the secp265k1_zkp::Tweak type to represent a blinding nonce in an issuance. This had a number of issues: * `Tweak` is a low-level crypto type which is ambiguous about whether or not it's secret or public, and doesn't have any meaning beyond "value you can add to a secp point" * In particular the special `ZERO_TWEAK` constant was interpreted to mean "new issuance" and this was implicit throughout the codebase. * As a foreign type, we could not directly implement any encoding or decoding traits on this; we had to convert it to/from a byte array. * As a "general" type, it doesn't distinguish between various uses of tweak, which can lead to dangerous type confusion. * `Tweak` cannot represent values that are out of range of the secp256k1 group, while technically Elements permits such transactions to exist (e.g. `decoderawtransaction` will accept them), even though it will fail VerifyAmounts. (I carefully checked all the codepaths: if the amounts in an issuance are null, so much be the nonce (or else you get a "superfluous issuance" error; if they're non-null and the blinding nonce is zero (which is in-range), it's a new issuance; if it's not zero then it's put into secp256k1_generator_generate_blinded which checks for overflow as its very first check.) --- src/genesis.rs | 5 +- src/issuance.rs | 100 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +- src/pset/map/input.rs | 20 ++++---- src/pset/serialize.rs | 18 ++----- src/transaction/decoders.rs | 42 ++++----------- src/transaction/encoders.rs | 4 +- src/transaction/mod.rs | 17 +++--- 8 files changed, 135 insertions(+), 76 deletions(-) diff --git a/src/genesis.rs b/src/genesis.rs index 112f90fe..d10367eb 100644 --- a/src/genesis.rs +++ b/src/genesis.rs @@ -15,12 +15,11 @@ //! Helpers to calculate the genesis block for a given network. use bitcoin::secp256k1::impl_array_newtype; -use secp256k1_zkp::Tweak; use crate::hashes::{sha256, HashEngine}; use crate::opcodes::all::OP_RETURN; use crate::opcodes::OP_TRUE; use crate::{confidential, script, AssetId, Block, BlockExtData, BlockHash, BlockHeader, LockTime, Script, Sequence, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness}; -use crate::{AssetEntropy, AssetIssuance, ContractHash, OutPoint, Txid}; +use crate::{AssetBlindingNonce, AssetEntropy, AssetIssuance, ContractHash, OutPoint, Txid}; use crate::confidential::Nonce; /// Parameters that influence chain consensus. The contents of the genesis block for a given network @@ -146,7 +145,7 @@ fn liquid_genesis_asset_tx(network_params: &NetworkParams) -> Option`, except +/// that there are no invalid values; [`AssetBlindingNonce::from_byte_array`] will +/// always succeed. However, if an out-of-range value is used, the transaction will +/// fail validation no matter what reissuance token is used. +/// +/// Also, **unlike [`AssetBlindingFactor`], this type represents public data**. You +/// can convert a blinding factor into a "blinding nonce", and while this conversion +/// is technically a no-op, conceptually it represents choosing to make the blinding +/// factor public. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Default)] +pub struct AssetBlindingNonce([u8; 32]); + +impl AssetBlindingNonce { + /// A null blinding nonce, representing a new issuance (vs a reissuance). + pub const NEW_ISSUANCE: Self = Self([0; 32]); + + /// Constructs this wrapper struct from raw bytes. + pub const fn from_byte_array(inner: [u8; 32]) -> Self { + Self(inner) + } + + /// The raw bytes within the wrapper type. + pub const fn as_byte_array(&self) -> &[u8; 32] { + &self.0 + } + + /// The raw bytes within the wrapper type. + pub const fn to_byte_array(self) -> [u8; 32] { + self.0 + } + + /// Whether this is the null "new issuance" blinding nonce. + pub fn is_null(&self) -> bool { + // This is surprisingly annoying to make into a constfn, so we don't + // bother for now. + *self == Self::NEW_ISSUANCE + } + + /// Reinterpret an asset blinding factor as a [`AssetBlindingNonce`]. + /// + /// This is something of a dangerous function, since in general blinding factors should + /// be considered secret data, while blinding nonces are public (they are encoded on + /// the blockchain). So callers of this function should be sure that this is a blinding + /// factor that they intend to reveal.) + pub fn from_blinding_factor(bf: AssetBlindingFactor) -> Self { + Self(*bf.into_inner().as_ref()) + } +} + +impl Encodable for AssetBlindingNonce { + fn consensus_encode(&self, e: W) -> Result { + self.0.consensus_encode(e) + } +} + +impl Decodable for AssetBlindingNonce { + fn consensus_decode(d: D) -> Result { + <[u8; 32]>::consensus_decode(d).map(Self) + } +} + +encoding::encoder_newtype_exact! { + /// Encoder for the [`OutPoint`] type. + #[derive(Clone, Debug)] + pub struct AssetBlindingNonceEncoder<'e>(encoding::ArrayRefEncoder<'e, 32>); +} + +impl encoding::Encode for AssetBlindingNonce { + type Encoder<'e> = AssetBlindingNonceEncoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + AssetBlindingNonceEncoder::new(encoding::ArrayRefEncoder::without_length_prefix(&self.0)) + } +} + +decoder_newtype! { + /// Decoder for the [`AssetBlindingNonce`] type. + #[derive(Default)] + pub struct AssetBlindingNonceDecoder(encoding::ArrayDecoder<32>); + + /// Decoder error for the [`AssetBlindingNonce`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct AssetBlindingNonceDecoderError(encoding::UnexpectedEofError); + const ERROR_DISPLAY = "error decoding asset blinding nonce"; + + impl Decode for AssetBlindingNonce { + fn convert_inner(bytes) -> Result<_, UnexpectedEofError> { + Ok(AssetBlindingNonce::from_byte_array(bytes)) + } + } +} + impl_sha256_midstate_wrapper! { /// An issued asset ID. pub struct AssetId([u8; 32]); diff --git a/src/lib.rs b/src/lib.rs index 87e8e864..55849309 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,8 +90,9 @@ pub use crate::ext::{ReadExt, WriteExt}; pub use crate::fast_merkle_root::fast_merkle_root; pub use crate::hash_types::*; pub use crate::issuance::{ - AssetEntropy, AssetEntropyDecoder, AssetEntropyDecoderError, AssetEntropyEncoder, AssetId, - ContractHash, + AssetBlindingNonce, AssetBlindingNonceDecoder, AssetBlindingNonceDecoderError, + AssetBlindingNonceEncoder, AssetEntropy, AssetEntropyDecoder, AssetEntropyDecoderError, + AssetEntropyEncoder, AssetId, ContractHash, }; pub use crate::locktime::LockTime; pub use crate::schnorr::{SchnorrSig, SchnorrSigError}; diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 98ec6734..9c0a65b3 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -32,10 +32,10 @@ use crate::pset::raw; use crate::pset::serialize; use crate::pset::{self, error, Error}; use crate::{transaction::SighashTypeParseError, SchnorrSighashType}; -use crate::{AssetIssuance, BlockHash, EcdsaSighashType, PeginWitness, RangeProof, Script, Transaction, TxIn, TxOut, Txid, SurjectionProof}; +use crate::{AssetBlindingNonce, AssetIssuance, BlockHash, EcdsaSighashType, PeginWitness, RangeProof, Script, Transaction, TxIn, TxOut, Txid, SurjectionProof}; use bitcoin::bip32::KeySource; use bitcoin::{PublicKey, key::XOnlyPublicKey}; -use secp256k1_zkp::{self, Tweak, ZERO_TWEAK}; +use secp256k1_zkp; use crate::{OutPoint, Sequence}; @@ -258,7 +258,7 @@ pub struct Input { /// Issuance inflation keys commitment pub issuance_inflation_keys_comm: Option, /// Issuance blinding nonce - pub issuance_blinding_nonce: Option, + pub issuance_blinding_nonce: Option, /// Issuance asset entropy pub issuance_asset_entropy: Option, /// input utxo rangeproof @@ -524,8 +524,8 @@ impl Input { /// Compute the issuance asset ids from pset. This function does not check /// whether there is an issuance in this input. Returns (`asset_id`, `token_id`) pub fn issuance_ids(&self) -> (AssetId, AssetId) { - let issue_nonce = self.issuance_blinding_nonce.unwrap_or(ZERO_TWEAK); - let entropy = if issue_nonce == ZERO_TWEAK { + let issue_nonce = self.issuance_blinding_nonce.unwrap_or_default(); + let entropy = if issue_nonce.is_null() { // new issuance let prevout = OutPoint { txid: self.previous_txid, @@ -560,7 +560,7 @@ impl Input { /// Get the issuance for this tx input pub fn asset_issuance(&self) -> AssetIssuance { AssetIssuance { - asset_blinding_nonce: *self.issuance_blinding_nonce.as_ref().unwrap_or(&ZERO_TWEAK), + asset_blinding_nonce: self.issuance_blinding_nonce.unwrap_or_default(), asset_entropy: self.issuance_asset_entropy.unwrap_or_default(), amount: match (self.issuance_value_amount, self.issuance_value_comm) { (None, None) => confidential::Value::Null, @@ -754,7 +754,7 @@ impl Map for Input { impl_pset_prop_insert_pair!(self.issuance_inflation_keys_comm <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_BLINDING_NONCE => { - impl_pset_prop_insert_pair!(self.issuance_blinding_nonce <= | ); + impl_pset_prop_insert_pair!(self.issuance_blinding_nonce <= | ); } PSBT_ELEMENTS_IN_ISSUANCE_ASSET_ENTROPY => { impl_pset_prop_insert_pair!(self.issuance_asset_entropy <= | ); @@ -1184,9 +1184,7 @@ where #[cfg(test)] mod tests { - use secp256k1_zkp::ZERO_TWEAK; - - use crate::confidential; + use crate::{AssetBlindingNonce, confidential}; use crate::pset::PartiallySignedTransaction; use crate::{AssetEntropy, AssetIssuance, LockTime, Transaction, TxIn, TxInWitness}; @@ -1198,7 +1196,7 @@ mod tests { fn from_tx_does_not_spuriously_set_proofs_on_explicit_issuance() { let txin = TxIn { asset_issuance: AssetIssuance { - asset_blinding_nonce: ZERO_TWEAK, + asset_blinding_nonce: AssetBlindingNonce::NEW_ISSUANCE, asset_entropy: DUMMY_ENTROPY, amount: confidential::Value::Explicit(1000), inflation_keys: confidential::Value::Null, diff --git a/src/pset/serialize.rs b/src/pset/serialize.rs index 0911d3e7..cf39eaca 100644 --- a/src/pset/serialize.rs +++ b/src/pset/serialize.rs @@ -24,12 +24,12 @@ use crate::encode::{ self, deserialize, deserialize_partial, serialize, Decodable, Encodable, VarInt, }; use crate::hashes::{hash160, ripemd160, sha256, sha256d, Hash}; -use crate::{AssetEntropy, AssetId, BlockHash, RangeProof, Script, SurjectionProof, Transaction, TxOut, Txid}; +use crate::{AssetBlindingNonce, AssetEntropy, AssetId, BlockHash, RangeProof, Script, SurjectionProof, Transaction, TxOut, Txid}; use bitcoin; use bitcoin::bip32::{ChildNumber, Fingerprint, KeySource}; use bitcoin::{key::XOnlyPublicKey, PublicKey}; use internals::slice::SliceExt; -use secp256k1_zkp::{self, Tweak}; +use secp256k1_zkp; use super::map::{PsbtSighashType, TapTree}; use crate::schnorr; @@ -59,6 +59,7 @@ pub fn serialize_hex(data: &T) -> String { impl_pset_de_serialize!(Transaction); impl_pset_de_serialize!(TxOut); +impl_pset_de_serialize!(AssetBlindingNonce); impl_pset_de_serialize!(AssetEntropy); impl_pset_de_serialize!(AssetId); impl_pset_de_serialize!(u8); @@ -109,19 +110,6 @@ impl Deserialize for VarInt { } } -impl Serialize for Tweak { - fn serialize(&self) -> Vec { - encode::serialize(self.as_ref()) - } -} - -impl Deserialize for Tweak { - fn deserialize(bytes: &[u8]) -> Result { - let x = deserialize::<[u8; 32]>(bytes)?; - Tweak::from_slice(&x).map_err(|_| encode::Error::ParseFailed("invalid Tweak")) - } -} - impl Serialize for AssetBlindingFactor { fn serialize(&self) -> Vec { encode::serialize(self.into_inner().as_ref()) diff --git a/src/transaction/decoders.rs b/src/transaction/decoders.rs index 1c9cf9c5..26df90e7 100644 --- a/src/transaction/decoders.rs +++ b/src/transaction/decoders.rs @@ -80,30 +80,26 @@ decoder_newtype! { /// Decoder for the [`AssetIssuance`] type. #[derive(Default)] pub struct AssetIssuanceDecoder(Decoder4< - ArrayDecoder<32>, + crate::AssetBlindingNonceDecoder, crate::AssetEntropyDecoder, crate::confidential::ValueDecoder, crate::confidential::ValueDecoder, >); /// Decoder error for the [`AssetIssuance`] type. #[derive(Clone, PartialEq, Eq, Debug)] - pub struct AssetIssuanceDecoderError(enum AssetIssuanceDecoderErrorInner { - Decode(Decoder4Error< - UnexpectedEofError, - crate::AssetEntropyDecoderError, - crate::confidential::ValueDecoderError, - crate::confidential::ValueDecoderError, - >), - InvalidTweak(secp256k1_zkp::Error), - }); + pub struct AssetIssuanceDecoderError(Decoder4Error< + crate::AssetBlindingNonceDecoderError, + crate::AssetEntropyDecoderError, + crate::confidential::ValueDecoderError, + crate::confidential::ValueDecoderError, + >); + const ERROR_DISPLAY = "error decoding asset issuance"; impl Decode for AssetIssuance { fn convert_inner(output) -> Result<_, AssetIssuanceDecoderError> { let (asset_blinding_nonce, asset_entropy, amount, inflation_keys) = output; Ok(AssetIssuance { - asset_blinding_nonce: secp256k1_zkp::Tweak::from_inner(asset_blinding_nonce) - .map_err(AssetIssuanceDecoderErrorInner::InvalidTweak) - .map_err(AssetIssuanceDecoderError)?, + asset_blinding_nonce, asset_entropy, amount, inflation_keys, @@ -112,26 +108,6 @@ decoder_newtype! { } } -impl fmt::Display for AssetIssuanceDecoderError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use AssetIssuanceDecoderErrorInner as Inner; - match self.0 { - Inner::Decode(_) => f.write_str("error decoding asset issuance"), - Inner::InvalidTweak(_) => f.write_str("asset issuance had out-of-range blinding nonce"), - } - } -} - -impl std::error::Error for AssetIssuanceDecoderError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use AssetIssuanceDecoderErrorInner as Inner; - match self.0 { - Inner::Decode(ref e) => Some(e), - Inner::InvalidTweak(ref e) => Some(e), - } - } -} - decoder_state_machine! { /// Decoder for the [`TxIn`] type. pub struct TxInDecoder(enum TxInDecoderInner { diff --git a/src/transaction/encoders.rs b/src/transaction/encoders.rs index ec8e66a5..0dc6164d 100644 --- a/src/transaction/encoders.rs +++ b/src/transaction/encoders.rs @@ -64,7 +64,7 @@ encoder_newtype_exact! { /// Encoder for the [`AssetIssuance`] type. #[derive(Clone, Debug)] pub struct AssetIssuanceEncoder<'e>(Encoder4< - ArrayRefEncoder<'e, 32>, + crate::AssetBlindingNonceEncoder<'e>, crate::AssetEntropyEncoder<'e>, crate::confidential::ValueEncoder<'e>, crate::confidential::ValueEncoder<'e>, @@ -76,7 +76,7 @@ impl Encode for AssetIssuance { fn encoder(&self) -> Self::Encoder<'_> { AssetIssuanceEncoder::new(Encoder4::new( - ArrayRefEncoder::without_length_prefix(self.asset_blinding_nonce.as_ref()), + self.asset_blinding_nonce.encoder(), self.asset_entropy.encoder(), self.amount.encoder(), self.inflation_keys.encoder(), diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index e8b88f2b..8b9ad701 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -30,14 +30,11 @@ use crate::hashes::{sha256d, HashEngine as _}; use crate::confidential; use crate::encode::{self, Encodable, Decodable}; -use crate::issuance::{AssetEntropy, AssetId}; +use crate::issuance::{AssetBlindingNonce, AssetEntropy, AssetId}; use crate::opcodes; use crate::parse::impl_parse_str_through_int; use crate::script::Instruction; use crate::{LockTime, RangeProof, Script, SurjectionProof, Txid, Wtxid}; -use secp256k1_zkp::{ - Tweak, ZERO_TWEAK, -}; pub use self::decoders::{AssetIssuanceDecoder, AssetIssuanceDecoderError, SequenceDecoder, SequenceDecoderError, TransactionDecoder, TransactionDecoderError, TxInDecoder, TxInDecoderError, TxInWitnessDecoder, TxInWitnessDecoderError, TxOutDecoder, TxOutDecoderError, TxOutWitnessDecoder, TxOutWitnessDecoderError}; pub use self::encoders::{AssetIssuanceEncoder, SequenceEncoder, TransactionEncoder, TxInEncoder, TxInWitnessEncoder, TxOutEncoder, TxOutWitnessEncoder}; @@ -50,7 +47,7 @@ pub use self::witness::{Witness, WitnessDecoder, WitnessDecoderError, WitnessEnc #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct AssetIssuance { /// Zero for a new asset issuance; otherwise a blinding factor for the input - pub asset_blinding_nonce: Tweak, + pub asset_blinding_nonce: AssetBlindingNonce, /// Freeform entropy field pub asset_entropy: AssetEntropy, /// Amount of asset to issue @@ -63,7 +60,7 @@ impl AssetIssuance { /// Create a null issuance. pub fn null() -> Self { AssetIssuance { - asset_blinding_nonce: ZERO_TWEAK, + asset_blinding_nonce: AssetBlindingNonce::NEW_ISSUANCE, asset_entropy: AssetEntropy::NEW_ISSUANCE, amount: confidential::Value::Null, inflation_keys: confidential::Value::Null, @@ -548,7 +545,7 @@ impl TxIn { /// Compute the issuance asset ids from this [`TxIn`]. This function does not check /// whether there is an issuance in this input. Returns (`asset_id`, `token_id`) pub fn issuance_ids(&self) -> (AssetId, AssetId) { - let entropy = if self.asset_issuance.asset_blinding_nonce == ZERO_TWEAK { + let entropy = if self.asset_issuance.asset_blinding_nonce.is_null() { let contract_hash = self .asset_issuance .asset_entropy @@ -1200,9 +1197,9 @@ impl ::std::error::Error for SighashTypeParseError {} mod tests { use std::str::FromStr; - use crate::{encode::serialize, pset::PartiallySignedTransaction}; + use crate::issuance::AssetBlindingNonce; +use crate::{encode::serialize, pset::PartiallySignedTransaction}; use crate::confidential; - use secp256k1_zkp::{self, ZERO_TWEAK}; use crate::script; use super::*; @@ -2047,7 +2044,7 @@ mod tests { assert_eq!( tx.input[0].asset_issuance, AssetIssuance { - asset_blinding_nonce: ZERO_TWEAK, + asset_blinding_nonce: AssetBlindingNonce::NEW_ISSUANCE, asset_entropy: AssetEntropy::NEW_ISSUANCE, amount: confidential::Value::from_commitment( &[ 0x09, 0x81, 0x65, 0x4e, 0xb5, 0xcc, 0xd9, 0x92, From 02122be963dc236a87ce443f54156aedca1fb01c Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Wed, 15 Jul 2026 21:48:18 +0000 Subject: [PATCH 4/4] issuance: add Encode/Decode impl for AssetID The only place this is actually "consensus encoded" is arguably in the pegin witness stack, but that's reason enough to have these. --- src/issuance.rs | 33 +++++++++++++++++++++++++++++++- src/lib.rs | 3 ++- src/transaction/pegin_witness.rs | 12 +++++++----- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/issuance.rs b/src/issuance.rs index 0c39deef..755be132 100644 --- a/src/issuance.rs +++ b/src/issuance.rs @@ -1,4 +1,4 @@ -// Rust Elements Library +// Rust Elements Libraryss // Written in 2019 by // The Elements developers // @@ -343,6 +343,37 @@ impl Decodable for AssetId { } } +encoding::encoder_newtype_exact! { + /// Encoder for the [`OutPoint`] type. + #[derive(Clone, Debug)] + pub struct AssetIdEncoder<'e>(encoding::ArrayRefEncoder<'e, 32>); +} + +impl encoding::Encode for AssetId { + type Encoder<'e> = AssetIdEncoder<'e>; + + fn encoder(&self) -> Self::Encoder<'_> { + AssetIdEncoder::new(encoding::ArrayRefEncoder::without_length_prefix(&self.0)) + } +} + +decoder_newtype! { + /// Decoder for the [`AssetId`] type. + #[derive(Default)] + pub struct AssetIdDecoder(encoding::ArrayDecoder<32>); + + /// Decoder error for the [`AssetId`] type. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct AssetIdDecoderError(encoding::UnexpectedEofError); + const ERROR_DISPLAY = "error decoding asset ID"; + + impl Decode for AssetId { + fn convert_inner(bytes) -> Result<_, UnexpectedEofError> { + Ok(AssetId::from_byte_array(bytes)) + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 55849309..ea5b713f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,7 +92,8 @@ pub use crate::hash_types::*; pub use crate::issuance::{ AssetBlindingNonce, AssetBlindingNonceDecoder, AssetBlindingNonceDecoderError, AssetBlindingNonceEncoder, AssetEntropy, AssetEntropyDecoder, AssetEntropyDecoderError, - AssetEntropyEncoder, AssetId, ContractHash, + AssetEntropyEncoder, AssetId, AssetIdDecoder, AssetIdDecoderError, AssetIdEncoder, + ContractHash, }; pub use crate::locktime::LockTime; pub use crate::schnorr::{SchnorrSig, SchnorrSigError}; diff --git a/src/transaction/pegin_witness.rs b/src/transaction/pegin_witness.rs index c80d66b8..9b2326fd 100644 --- a/src/transaction/pegin_witness.rs +++ b/src/transaction/pegin_witness.rs @@ -347,7 +347,7 @@ pub struct PeginDataDecoder { encoding::ArrayDecoder<8>, // asset ID ExactLengthDecoder, - encoding::ArrayDecoder<32>, + crate::AssetIdDecoder, // genesis hash ExactLengthDecoder, GenesisHashDecoder, @@ -372,7 +372,7 @@ impl Default for PeginDataDecoder { ExactLengthDecoder::new(8), encoding::ArrayDecoder::new(), ExactLengthDecoder::new(32), - encoding::ArrayDecoder::new(), + crate::AssetIdDecoder::default(), ExactLengthDecoder::new(32), GenesisHashDecoder::new(), ), @@ -388,6 +388,7 @@ impl Default for PeginDataDecoder { #[derive(Clone, PartialEq, Eq, Debug)] enum PeginDataDecoderErrorInner { + AssetId(crate::AssetIdDecoderError), ClaimScript(bitcoin::blockdata::script::ScriptBufDecoderError), GenesisHash(GenesisHashDecoderError), Length(ExactLengthDecoderError), @@ -421,6 +422,7 @@ impl std::error::Error for PeginWitnessDecoderError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { use PeginDataDecoderErrorInner as Inner; match self.inner { + Inner::AssetId(ref e) => Some(e), Inner::ClaimScript(ref e) => Some(e), Inner::GenesisHash(ref e) => Some(e), Inner::Length(ref e) => Some(e), @@ -441,7 +443,7 @@ impl PeginWitnessDecoderError { ExactLengthDecoderError, UnexpectedEofError, ExactLengthDecoderError, - UnexpectedEofError, + crate::AssetIdDecoderError, ExactLengthDecoderError, GenesisHashDecoderError, >, @@ -464,7 +466,7 @@ impl PeginWitnessDecoderError { Dec3Err::Second(Dec6Err::Third(error)) => Self { field: "asset ID", inner: PeginDataDecoderErrorInner::Length(error) }, Dec3Err::Second(Dec6Err::Fourth(error)) => - Self { field: "asset ID", inner: PeginDataDecoderErrorInner::Eof(error) }, + Self { field: "asset ID", inner: PeginDataDecoderErrorInner::AssetId(error) }, Dec3Err::Second(Dec6Err::Fifth(error)) => Self { field: "genesis hash", inner: PeginDataDecoderErrorInner::Length(error) }, Dec3Err::Second(Dec6Err::Sixth(error)) => Self { @@ -514,7 +516,7 @@ impl encoding::Decoder for PeginDataDecoder { Ok(PeginData { value: u64::from_le_bytes(value), - asset_id: AssetId::from_byte_array(asset_id), + asset_id, genesis_hash, claim_script, transaction,