Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions SEQUENTIA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lwk_bindings/src/pset/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 2 additions & 5 deletions lwk_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion lwk_common/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 67 additions & 0 deletions lwk_signer/src/software.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion lwk_wasm/src/pset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lwk_wollet/src/pset_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 88 additions & 11 deletions rust-elements/src/pset/map/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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(),
}
}
}
Expand Down
61 changes: 59 additions & 2 deletions rust-elements/src/pset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 =
Expand Down
Loading