diff --git a/src/pset/error.rs b/src/pset/error.rs index 46d8f283..318bcece 100644 --- a/src/pset/error.rs +++ b/src/pset/error.rs @@ -250,6 +250,8 @@ pub enum PsetBlindError { ConfidentialTxOutError(usize, ConfidentialTxOutError), /// Blinding proof creation error BlindingProofsCreationError(usize, secp256k1_zkp::Error), + /// Blinding issuance unsupported + BlindingIssuanceUnsupported(usize), } impl fmt::Display for PsetBlindError { @@ -291,6 +293,9 @@ impl fmt::Display for PsetBlindError { e, i ) } + PsetBlindError::BlindingIssuanceUnsupported(i) => { + write!(f, "Blinding issuance is not supported, set blinded_issuance to 0 at input index {}", i) + } } } } diff --git a/src/pset/map/input.rs b/src/pset/map/input.rs index 7aade0ea..618f2eef 100644 --- a/src/pset/map/input.rs +++ b/src/pset/map/input.rs @@ -163,6 +163,11 @@ const PSBT_ELEMENTS_IN_EXPLICIT_ASSET: u8 = 0x13; /// the explicit asset in PSBT_ELEMENTS_IN_EXPLICIT_ASSET. If provided, /// PSBT_ELEMENTS_IN_EXPLICIT_ASSET must be provided too. const PSBT_ELEMENTS_IN_ASSET_PROOF: u8 = 0x14; +/// A boolean flag. 0x00 indicates the issuance should not be blinded, +/// 0x01 indicates it should be. If not specified, assumed to be 0x01. +/// Note that this does not indicate actual blinding status, +/// but rather the expected blinding status prior to signing. +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)] @@ -294,6 +299,8 @@ pub struct Input { pub asset: Option, /// The blind asset surjection proof pub blind_asset_proof: Option>, + /// Whether the issuance is blinded + pub blinded_issuance: Option, /// Other fields #[cfg_attr( feature = "serde", @@ -310,7 +317,7 @@ pub struct Input { impl Default for Input { fn default() -> Self { - Self { non_witness_utxo: Default::default(), witness_utxo: Default::default(), partial_sigs: Default::default(), sighash_type: Default::default(), redeem_script: Default::default(), witness_script: Default::default(), bip32_derivation: Default::default(), final_script_sig: Default::default(), final_script_witness: Default::default(), ripemd160_preimages: Default::default(), sha256_preimages: Default::default(), hash160_preimages: Default::default(), hash256_preimages: Default::default(), previous_txid: Txid::all_zeros(), previous_output_index: Default::default(), sequence: Default::default(), required_time_locktime: Default::default(), required_height_locktime: Default::default(), tap_key_sig: Default::default(), tap_script_sigs: Default::default(), tap_scripts: Default::default(), tap_key_origins: Default::default(), tap_internal_key: Default::default(), tap_merkle_root: Default::default(), issuance_value_amount: Default::default(), issuance_value_comm: Default::default(), issuance_value_rangeproof: Default::default(), issuance_keys_rangeproof: Default::default(), pegin_tx: Default::default(), pegin_txout_proof: Default::default(), pegin_genesis_hash: Default::default(), pegin_claim_script: Default::default(), pegin_value: Default::default(), pegin_witness: Default::default(), issuance_inflation_keys: Default::default(), issuance_inflation_keys_comm: Default::default(), issuance_blinding_nonce: Default::default(), issuance_asset_entropy: Default::default(), in_utxo_rangeproof: Default::default(), in_issuance_blind_value_proof: Default::default(), in_issuance_blind_inflation_keys_proof: Default::default(), amount: Default::default(), blind_value_proof: Default::default(), asset: Default::default(), blind_asset_proof: Default::default(), proprietary: Default::default(), unknown: Default::default() } + Self { non_witness_utxo: Default::default(), witness_utxo: Default::default(), partial_sigs: Default::default(), sighash_type: Default::default(), redeem_script: Default::default(), witness_script: Default::default(), bip32_derivation: Default::default(), final_script_sig: Default::default(), final_script_witness: Default::default(), ripemd160_preimages: Default::default(), sha256_preimages: Default::default(), hash160_preimages: Default::default(), hash256_preimages: Default::default(), previous_txid: Txid::all_zeros(), previous_output_index: Default::default(), sequence: Default::default(), required_time_locktime: Default::default(), required_height_locktime: Default::default(), tap_key_sig: Default::default(), tap_script_sigs: Default::default(), tap_scripts: Default::default(), tap_key_origins: Default::default(), tap_internal_key: Default::default(), tap_merkle_root: Default::default(), issuance_value_amount: Default::default(), issuance_value_comm: Default::default(), issuance_value_rangeproof: Default::default(), issuance_keys_rangeproof: Default::default(), pegin_tx: Default::default(), pegin_txout_proof: Default::default(), pegin_genesis_hash: Default::default(), pegin_claim_script: Default::default(), pegin_value: Default::default(), pegin_witness: Default::default(), issuance_inflation_keys: Default::default(), issuance_inflation_keys_comm: Default::default(), issuance_blinding_nonce: Default::default(), issuance_asset_entropy: Default::default(), in_utxo_rangeproof: Default::default(), in_issuance_blind_value_proof: Default::default(), in_issuance_blind_inflation_keys_proof: Default::default(), amount: Default::default(), blind_value_proof: Default::default(), asset: Default::default(), blind_asset_proof: Default::default(), blinded_issuance: Default::default(), proprietary: Default::default(), unknown: Default::default() } } } @@ -740,6 +747,9 @@ impl Map for Input { PSBT_ELEMENTS_IN_ASSET_PROOF => { impl_pset_prop_insert_pair!(self.blind_asset_proof <= | >) } + PSBT_ELEMENTS_IN_BLINDED_ISSUANCE => { + impl_pset_prop_insert_pair!(self.blinded_issuance <= | ) + } _ => match self.proprietary.entry(prop_key) { Entry::Vacant(empty_key) => { empty_key.insert(raw_value); @@ -954,6 +964,10 @@ impl Map for Input { rv.push_prop(self.blind_asset_proof as ) } + impl_pset_get_pair! { + rv.push_prop(self.blinded_issuance as ) + } + for (key, value) in self.proprietary.iter() { rv.push(raw::Pair { key: key.to_key(), @@ -1028,6 +1042,11 @@ impl Map for Input { merge!(in_utxo_rangeproof, self, other); merge!(in_issuance_blind_value_proof, self, other); merge!(in_issuance_blind_inflation_keys_proof, self, other); + merge!(amount, self, other); + merge!(blind_value_proof, self, other); + merge!(asset, self, other); + merge!(blind_asset_proof, self, other); + merge!(blinded_issuance, self, other); Ok(()) } } diff --git a/src/pset/mod.rs b/src/pset/mod.rs index 696ee2a0..351202e0 100644 --- a/src/pset/mod.rs +++ b/src/pset/mod.rs @@ -373,6 +373,11 @@ impl PartiallySignedTransaction { ), PsetBlindError, > { + for (i, inp) in self.inputs.iter().enumerate() { + if inp.has_issuance() && inp.blinded_issuance.unwrap_or(1) == 1 { + return Err(PsetBlindError::BlindingIssuanceUnsupported(i)); + } + } let mut blind_out_indices = Vec::new(); for (i, out) in self.outputs.iter().enumerate() { if out.blinding_key.is_none() { @@ -971,4 +976,71 @@ mod tests { assert_eq!(pset.n_inputs(), n_inputs - 1); assert_eq!(pset.n_outputs(), n_outputs - 1); } + + #[test] + fn pset_issuance() { + use std::str::FromStr; + use rand::{self, SeedableRng}; + let secp = secp256k1_zkp::Secp256k1::new(); + #[allow(deprecated)] + let mut rng = rand::rngs::StdRng::seed_from_u64(0); + + let policy = crate::AssetId::from_str("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225").unwrap(); + let pk = bitcoin::key::PublicKey::from_str("020202020202020202020202020202020202020202020202020202020202020202").unwrap(); + let script = crate::Script::from_hex("0014d2bcde17e7744f6377466ca1bd35d212954674c8").unwrap(); + let sats_in = 10000; + let sats_fee = 1000; + let btc_txout_secrets = TxOutSecrets { + asset_bf: AssetBlindingFactor::from_str("1111111111111111111111111111111111111111111111111111111111111111").unwrap(), + value_bf: ValueBlindingFactor::from_str("2222222222222222222222222222222222222222222222222222222222222222").unwrap(), + value: sats_in, + asset: policy, + }; + let previous_output = TxOut::default(); // Does not match btc_txout_secrets + let prevout = OutPoint::default(); + let sats_asset = 10; + let sats_token = 1; + + let mut pset = PartiallySignedTransaction::new_v2(); + let mut input = Input::from_prevout(prevout); + input.witness_utxo = Some(previous_output); + input.issuance_value_amount = Some(sats_asset); + input.issuance_inflation_keys = Some(sats_token); + let (asset, token) = input.issuance_ids(); + pset.add_input(input); + + // Add asset + let mut output = Output::new_explicit(script.clone(), sats_asset, asset, Some(pk)); + output.blinder_index = Some(0); + pset.add_output(output); + // Add token + let mut output = Output::new_explicit(script.clone(), sats_token, token, Some(pk)); + output.blinder_index = Some(0); + pset.add_output(output); + // Add L-BTC + let mut output = Output::new_explicit(script.clone(), sats_in - sats_fee, policy, Some(pk)); + output.blinder_index = Some(0); + pset.add_output(output); + // Add fee + let output = Output::new_explicit(crate::Script::new(), sats_fee, policy, None); + pset.add_output(output); + + let mut inp_txout_sec = HashMap::new(); + inp_txout_sec.insert(0, btc_txout_secrets); + + let err = pset.blind_last(&mut rng, &secp, &inp_txout_sec).unwrap_err(); + assert_eq!(err, PsetBlindError::BlindingIssuanceUnsupported(0)); + + let input = &mut pset.inputs_mut()[0]; + input.blinded_issuance = Some(0x01); + let err = pset.blind_last(&mut rng, &secp, &inp_txout_sec).unwrap_err(); + assert_eq!(err, PsetBlindError::BlindingIssuanceUnsupported(0)); + + let input = &mut pset.inputs_mut()[0]; + input.blinded_issuance = Some(0x00); + pset.blind_last(&mut rng, &secp, &inp_txout_sec).unwrap(); + let pset_bytes = encode::serialize(&pset); + let pset_des = encode::deserialize(&pset_bytes).unwrap(); + assert_eq!(pset, pset_des); + } }