diff --git a/SEQUENTIA.md b/SEQUENTIA.md index 52113a5c4..d7139427d 100644 --- a/SEQUENTIA.md +++ b/SEQUENTIA.md @@ -63,6 +63,17 @@ its `sequentia` cargo feature: `src/sighash.rs`, `src/blind.rs` touch points): Sequentia issuance transactions carry an extra `nDenomination` byte; the feature adds it to issuance (de)serialization, PSET input mapping, and sighash computation. + The PSET format has no field for it, so a PSET input keeps it in a + proprietary key (prefix `sequentia`, subtype `0x00`, one byte; + `Input::issuance_denomination`, `set_issuance_denomination`), written only + when it is not the node's default, 8. `Input::from_txin` carries it from the + transaction, and the transaction a PSET extracts has it, so a PSET made from + a transaction is signed over that transaction. +- `src/pset/map/input.rs`, `src/pset/mod.rs`: an input's issuance is not + flagged in its output index (`Input::from_txin`), as Elements Core's PSET + does not flag it, and `Input::previous_outpoint()` masks off any flag a PSET + carries there. The extracted transaction and the issued asset's id use that + outpoint, so an issuance input is signed over the outpoint it spends. - `src/address.rs`: `AddressParams::SEQUENTIA_TESTNET` (base58 p2pkh 111 / p2sh 196 / blinded 70; bech32 HRP `tb`; blech32 HRP `tsqb`) and address-string parsing for those prefixes. Sequentia is transparent by default: the default diff --git a/lwk_bindings/src/pset/input.rs b/lwk_bindings/src/pset/input.rs index 30dc14fa2..1a2c5abc8 100644 --- a/lwk_bindings/src/pset/input.rs +++ b/lwk_bindings/src/pset/input.rs @@ -45,7 +45,7 @@ impl PsetInput { /// Prevout vout of the input. pub fn previous_vout(&self) -> u32 { - self.inner.previous_output_index + self.inner.previous_outpoint().vout } /// Prevout scriptpubkey of the input. diff --git a/lwk_common/src/lib.rs b/lwk_common/src/lib.rs index 6132f1ac6..d12e8b663 100644 --- a/lwk_common/src/lib.rs +++ b/lwk_common/src/lib.rs @@ -85,7 +85,7 @@ use elements_miniscript::elements::{ pset::PartiallySignedTransaction, script::Builder, secp256k1_zkp::{All, Generator, PedersenCommitment, Secp256k1}, - AssetId, BlindAssetProofs, BlindValueProofs, OutPoint, Script, TxOutSecrets, + AssetId, BlindAssetProofs, BlindValueProofs, Script, TxOutSecrets, }; use elements_miniscript::{ConfidentialDescriptor, DescriptorPublicKey}; use std::collections::btree_map::BTreeMap; @@ -194,10 +194,7 @@ pub fn pset_balance( for (idx, input) in pset.inputs().iter().enumerate() { match input.witness_utxo.as_ref() { None => { - let previous_outpoint = OutPoint { - txid: input.previous_txid, - vout: input.previous_output_index, - }; + let previous_outpoint = input.previous_outpoint(); return Err(Error::MissingPreviousOutput { idx, previous_outpoint, diff --git a/lwk_common/src/model.rs b/lwk_common/src/model.rs index 2bb31cb13..2a90b43dc 100644 --- a/lwk_common/src/model.rs +++ b/lwk_common/src/model.rs @@ -75,7 +75,7 @@ impl Issuance { // These are meaningless if inner is null let (asset, token) = input.issuance_ids(); - let prev_output = OutPoint::new(input.previous_txid, input.previous_output_index); + let prev_output = input.previous_outpoint(); Self { asset, token, diff --git a/lwk_signer/src/software.rs b/lwk_signer/src/software.rs index 212ec8b4e..e5935ec3f 100644 --- a/lwk_signer/src/software.rs +++ b/lwk_signer/src/software.rs @@ -568,6 +568,73 @@ mod tests { assert!(sig_low_r.len() < sig_no_grind.len()); } + /// A caller holding a whole transaction signs it through a PSET made + /// from it. For an input that issues an asset, the signature must cover + /// that transaction: the outpoint it spends and its issuance, whose + /// denomination Sequentia serializes and so signs over. + #[test] + fn an_issuing_input_is_signed_over_its_own_transaction() { + use elements_miniscript::elements::{ + confidential, hashes::hash160, secp256k1_zkp::ecdsa, AssetId, AssetIssuance, LockTime, + OutPoint, PubkeyHash, Script, Sequence, Transaction, TxIn, TxOut, Txid, WPubkeyHash, + }; + let signer = SwSigner::new(lwk_test_util::TEST_MNEMONIC, false).unwrap(); + let path: DerivationPath = "m/84'/1'/0'/0/0".parse().unwrap(); + let pk = bitcoin::PublicKey::new(signer.derive_xpub(&path).unwrap().public_key); + let h = hash160::Hash::hash(&pk.to_bytes()).to_byte_array(); + let spk = Script::new_v0_wpkh(&WPubkeyHash::from_byte_array(h)); + let script_code = Script::new_p2pkh(&PubkeyHash::from_byte_array(h)); + let asset = AssetId::from_slice(&[9; 32]).unwrap(); + let value = 10_000; + for (vout, denomination) in [(0, 8), (1, 8), (1, 0), (3, 2)] { + let input = TxIn { + previous_output: OutPoint::new(Txid::from_byte_array([7; 32]), vout), + sequence: Sequence::MAX, + asset_issuance: AssetIssuance { + amount: confidential::Value::Explicit(1), + denomination, + ..AssetIssuance::null() + }, + ..Default::default() + }; + let tx = Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![input], + output: vec![TxOut::new_fee(value, asset)], + }; + let mut pset = PartiallySignedTransaction::from_tx(tx.clone()); + let utxo = TxOut { + asset: confidential::Asset::Explicit(asset), + value: confidential::Value::Explicit(value), + nonce: confidential::Nonce::Null, + script_pubkey: spk.clone(), + witness: Default::default(), + }; + pset.inputs_mut()[0].witness_utxo = Some(utxo); + pset.inputs_mut()[0] + .bip32_derivation + .insert(pk, (signer.fingerprint(), path.clone())); + assert_eq!(signer.sign(&mut pset).unwrap(), 1); + let raw = pset.inputs()[0].partial_sigs[&pk].clone(); + let sig = ecdsa::Signature::from_der(&raw[..raw.len() - 1]).unwrap(); + // The signature hash of the transaction itself. + let digest = SighashCache::new(&tx).segwitv0_sighash( + 0, + &script_code, + confidential::Value::Explicit(value), + EcdsaSighashType::All, + ); + let msg = Message::from_digest(digest.to_byte_array()); + signer + .secp + .verify_ecdsa(&msg, &sig, &pk.inner) + .unwrap_or_else(|e| { + panic!("vout {vout}, denomination {denomination}: the signature does not cover the transaction: {e}") + }); + } + } + #[test] fn test_sign_verify() { let signer = SwSigner::new(lwk_test_util::TEST_MNEMONIC, true).unwrap(); diff --git a/lwk_wasm/src/pset.rs b/lwk_wasm/src/pset.rs index 698331af6..b98abc81d 100644 --- a/lwk_wasm/src/pset.rs +++ b/lwk_wasm/src/pset.rs @@ -156,7 +156,7 @@ impl PsetInput { /// Prevout vout of the input #[wasm_bindgen(js_name = previousVout)] pub fn previous_vout(&self) -> u32 { - self.inner.previous_output_index + self.inner.previous_outpoint().vout } /// Prevout scriptpubkey of the input diff --git a/lwk_wollet/src/pset_create.rs b/lwk_wollet/src/pset_create.rs index 8ab9b9c61..e506db8ee 100644 --- a/lwk_wollet/src/pset_create.rs +++ b/lwk_wollet/src/pset_create.rs @@ -113,7 +113,7 @@ impl Wollet { let (asset, token) = input.issuance_ids(); if let Some(contract) = contract.as_ref() { - let issuance_prevout = OutPoint::new(input.previous_txid, input.previous_output_index); + let issuance_prevout = input.previous_outpoint(); let contract = serde_json::to_string(&contract)?; pset.add_asset_metadata(asset, &AssetMetadata::new(contract, issuance_prevout)); // TODO: handle blinded issuance diff --git a/rust-elements/src/pset/map/input.rs b/rust-elements/src/pset/map/input.rs index 13bc22492..545ab88c3 100644 --- a/rust-elements/src/pset/map/input.rs +++ b/rust-elements/src/pset/map/input.rs @@ -168,6 +168,36 @@ const PSBT_ELEMENTS_IN_ASSET_PROOF: u8 = 0x14; /// 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; + +/// Bit of [`Input::previous_output_index`] marking a pegin input. +const OUTPOINT_PEGIN_FLAG: u32 = 1 << 30; +/// Bit of [`Input::previous_output_index`] some writers set to mark an +/// issuance; never set here, and masked off when read. +const OUTPOINT_ISSUANCE_FLAG: u32 = 1 << 31; +/// The output index of a coinbase input: all 1's, and no flags. +const OUTPOINT_COINBASE_INDEX: u32 = 0xffff_ffff; + +/// SEQUENTIA: the prefix of the proprietary keys this crate adds to a PSET +/// input for what Sequentia's transactions carry and the PSET format does not. +#[cfg(feature = "sequentia")] +pub const SEQUENTIA_PROPRIETARY_PREFIX: &[u8] = b"sequentia"; +/// SEQUENTIA: the proprietary subtype holding an issuance's denomination, one +/// byte ([`Input::issuance_denomination`]). +#[cfg(feature = "sequentia")] +pub const SEQUENTIA_IN_ISSUANCE_DENOMINATION: u8 = 0x00; +/// SEQUENTIA: the denomination of an issuance that names none, as the node's +/// `CAssetIssuance` sets it. +#[cfg(feature = "sequentia")] +const DEFAULT_DENOMINATION: u8 = 8; + +#[cfg(feature = "sequentia")] +fn denomination_key() -> raw::ProprietaryKey { + raw::ProprietaryKey { + prefix: SEQUENTIA_PROPRIETARY_PREFIX.to_vec(), + subtype: SEQUENTIA_IN_ISSUANCE_DENOMINATION.into(), + key: vec![], + } +} /// A key-value map for an input of the corresponding index in the unsigned /// transaction. #[derive(Clone, Debug, PartialEq)] @@ -465,11 +495,17 @@ impl Input { ret.final_script_witness = Some(txin.witness.script_witness); if txin.is_pegin { - ret.previous_output_index |= 1 << 30; + ret.previous_output_index |= OUTPOINT_PEGIN_FLAG; ret.pegin_witness = Some(txin.witness.pegin_witness); } if has_issuance { - ret.previous_output_index |= 1 << 31; + // SEQUENTIA: the issuance is not flagged in the output index, as + // Elements Core's PSET does not flag it: the index is the + // outpoint's, and the issuance fields say there is one. A flag + // here named another outpoint in the transaction extracted to + // sign, and in the issued asset's id. + #[cfg(feature = "sequentia")] + ret.set_issuance_denomination(txin.asset_issuance.denomination); ret.issuance_blinding_nonce = Some(txin.asset_issuance.asset_blinding_nonce); ret.issuance_asset_entropy = Some(txin.asset_issuance.asset_entropy); match txin.asset_issuance.amount { @@ -492,16 +528,33 @@ impl Input { ret } + /// The outpoint spent by this input. + /// + /// Flags a PSET may carry in the high bits of + /// [`Input::previous_output_index`] (the pegin flag, and the issuance + /// flag some writers set) are masked off; use [`Input::is_pegin`] and + /// [`Input::has_issuance`] to query them. An all-1's index (a coinbase + /// input) has no flags and is returned as it is. + pub fn previous_outpoint(&self) -> OutPoint { + let vout = if self.previous_output_index == OUTPOINT_COINBASE_INDEX { + self.previous_output_index + } else { + self.previous_output_index & !(OUTPOINT_PEGIN_FLAG | OUTPOINT_ISSUANCE_FLAG) + }; + OutPoint { + txid: self.previous_txid, + vout, + } + } + /// 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 { - // new issuance - let prevout = OutPoint { - txid: self.previous_txid, - vout: self.previous_output_index, - }; + // new issuance; the entropy commits to the outpoint spent, without + // flags + let prevout = self.previous_outpoint(); let contract_hash = ContractHash::from_byte_array(self.issuance_asset_entropy.unwrap_or_default()); AssetId::generate_asset_entropy(prevout, contract_hash) @@ -523,7 +576,33 @@ impl Input { /// If the Pset Input is pegin pub fn is_pegin(&self) -> bool { - self.previous_output_index & (1 << 30) != 0 + self.previous_output_index & OUTPOINT_PEGIN_FLAG != 0 + } + + /// SEQUENTIA: the denomination (decimal places) of this input's + /// issuance. Sequentia serializes it after the inflation keys, so it is + /// part of what a signature covers. The PSET format has no field for it; + /// it is kept in a proprietary key ([`SEQUENTIA_PROPRIETARY_PREFIX`], + /// subtype [`SEQUENTIA_IN_ISSUANCE_DENOMINATION`]), and an input without + /// that key has the node's default, 8. + #[cfg(feature = "sequentia")] + pub fn issuance_denomination(&self) -> u8 { + match self.proprietary.get(&denomination_key()) { + Some(v) if v.len() == 1 => v[0], + _ => DEFAULT_DENOMINATION, + } + } + + /// SEQUENTIA: set the denomination of this input's issuance + /// ([`Input::issuance_denomination`]). The default, 8, is kept as no key, + /// so a PSET whose issuances all have it is serialized as before. + #[cfg(feature = "sequentia")] + pub fn set_issuance_denomination(&mut self, denomination: u8) { + if denomination == DEFAULT_DENOMINATION { + self.proprietary.remove(&denomination_key()); + } else { + self.proprietary.insert(denomination_key(), vec![denomination]); + } } /// Get the issuance for this tx input @@ -544,10 +623,8 @@ impl Input { (_, Some(comm)) => confidential::Value::Confidential(comm), (Some(x), None) => confidential::Value::Explicit(x), }, - // SEQUENTIA: PSET does not (yet) carry the issuance denomination; - // default to 8 to match AssetIssuance::null(). #[cfg(feature = "sequentia")] - denomination: 8, + denomination: self.issuance_denomination(), } } } diff --git a/rust-elements/src/pset/mod.rs b/rust-elements/src/pset/mod.rs index a53ee2e35..62d507047 100644 --- a/rust-elements/src/pset/mod.rs +++ b/rust-elements/src/pset/mod.rs @@ -46,7 +46,7 @@ use crate::{ confidential::{AssetBlindingFactor, ValueBlindingFactor}, TxOutSecrets, }; -use crate::{OutPoint, LockTime, Sequence, SurjectionInput, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, Txid}; +use crate::{LockTime, Sequence, SurjectionInput, Transaction, TxIn, TxInWitness, TxOut, TxOutWitness, Txid}; use secp256k1_zkp::rand::{CryptoRng, RngCore}; use secp256k1_zkp::{self, RangeProof, SecretKey, SurjectionProof}; @@ -285,7 +285,7 @@ impl PartiallySignedTransaction { for psetin in self.inputs.iter() { let txin = TxIn { - previous_output: OutPoint::new(psetin.previous_txid, psetin.previous_output_index), + previous_output: psetin.previous_outpoint(), is_pegin: psetin.is_pegin(), script_sig: psetin.final_script_sig.clone().unwrap_or_default(), sequence: psetin.sequence.unwrap_or(Sequence::MAX), @@ -773,6 +773,63 @@ impl Decodable for PartiallySignedTransaction { mod tests { use super::*; use crate::hex::{FromHex, ToHex}; + use crate::OutPoint; + + /// A transaction with one input, at output `vout` of a made-up + /// transaction, that issues one explicit atom of a new asset with + /// `denomination` decimal places. + #[cfg(feature = "sequentia")] + fn issuing_tx(vout: u32, denomination: u8) -> Transaction { + use crate::hashes::Hash; + use crate::{AssetId, AssetIssuance}; + let issuance = AssetIssuance { + amount: confidential::Value::Explicit(1), + denomination, + ..AssetIssuance::null() + }; + let input = TxIn { + previous_output: OutPoint::new(Txid::from_byte_array([7; 32]), vout), + sequence: Sequence::MAX, + asset_issuance: issuance, + ..Default::default() + }; + let fee = TxOut::new_fee(1_000, AssetId::from_slice(&[9; 32]).unwrap()); + Transaction { version: 2, lock_time: LockTime::ZERO, input: vec![input], output: vec![fee] } + } + + // SEQUENTIA: a PSET made from a transaction with an issuance input + // extracts that same transaction: the outpoint it spends (the issuance is + // no flag of the output index) and the issuance's denomination, which + // Sequentia serializes after the inflation keys and so signs over. + #[test] + #[cfg(feature = "sequentia")] + fn an_issuance_input_keeps_its_outpoint() { + for vout in [0, 1, 5] { + let tx = issuing_tx(vout, 8); + let pset = PartiallySignedTransaction::from_tx(tx.clone()); + let back = pset.extract_tx().unwrap(); + assert_eq!(back.input[0].previous_output, tx.input[0].previous_output, "vout {vout}"); + assert_eq!(back.txid(), tx.txid()); + // The issued asset's id commits to the outpoint spent. + assert_eq!(pset.inputs()[0].issuance_ids(), tx.input[0].issuance_ids()); + } + } + + #[test] + #[cfg(feature = "sequentia")] + fn an_issuance_input_keeps_its_denomination() { + for denomination in [0, 2, 8] { + let tx = issuing_tx(1, denomination); + let pset = PartiallySignedTransaction::from_tx(tx.clone()); + let back = pset.extract_tx().unwrap(); + assert_eq!(back.input[0].asset_issuance.denomination, denomination); + assert_eq!(back.input[0].asset_issuance, tx.input[0].asset_issuance); + // Through the PSET's serialized form too. + let bytes = encode::serialize(&pset); + let read: PartiallySignedTransaction = encode::deserialize(&bytes).unwrap(); + assert_eq!(read.extract_tx().unwrap().input[0].asset_issuance.denomination, denomination); + } + } fn tx_pset_rtt(tx_hex: &str) { let tx: Transaction =