Skip to content

Commit

Permalink
Add PSBT v2
Browse files Browse the repository at this point in the history
Add an implementation of PSBT version 2 - BOOM!

Includes all test vectors from BIP-370.
  • Loading branch information
tcharding committed Nov 30, 2023
1 parent 4c845f8 commit 5e11818
Show file tree
Hide file tree
Showing 23 changed files with 5,453 additions and 89 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ secp256k1 = { version = "0.28", features = ["rand-std", "global-context"] }
[[example]]
name = "v0-2-of-2-multisig"
required-features = ["miniscript-std"]

[[example]]
name = "v2"
required-features = ["miniscript-std"]

[[example]]
name = "v2-separate-creator-constructor"
62 changes: 62 additions & 0 deletions examples/v2-separate-creator-constructor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! PSBT v2 - Creator a PSBT and hand it around to various different entities to add inputs and outputs.
use psbt::bitcoin::hashes::Hash as _;
use psbt::bitcoin::{Amount, OutPoint, ScriptBuf, TxOut, Txid};
use psbt::v2::{
Constructor, Creator, InputBuilder, InputsOnlyModifiable, OutputBuilder, OutputsOnlyModifiable,
Psbt,
};

fn main() -> anyhow::Result<()> {
// Create the PSBT.
let created = Creator::new().inputs_modifiable().outputs_modifiable().psbt();

let ser = created.serialize();

// The first constructor entity receives the PSBT and adds an input.
let psbt = Psbt::deserialize(&ser)?;
let in_0 = dummy_out_point();
let ser = Constructor::<InputsOnlyModifiable>::new(psbt)?
.input(InputBuilder::new(in_0).build())
.psbt()
.serialize();

// The second constructor entity receives the PSBT with one input and adds a second input.
let psbt = Psbt::deserialize(&ser)?;
let in_1 = dummy_out_point();
let ser = Constructor::<InputsOnlyModifiable>::new(psbt)?
.input(InputBuilder::new(in_1).build())
.no_more_inputs()
.psbt()
.serialize();

// The third constructor entity receives the PSBT with inputs and adds an output.
let psbt = Psbt::deserialize(&ser)?;
let output = dummy_tx_out();
let ser = Constructor::<OutputsOnlyModifiable>::new(psbt)?
.output(OutputBuilder::new(output).build())
.no_more_outputs()
.psbt()
.serialize();

// The PSBT is now ready for handling with the updater role.
let _updatable_psbt = Psbt::deserialize(&ser)?;

Ok(())
}

/// A dummy `OutPoint`, this would usually be the unspent transaction that we are spending.
fn dummy_out_point() -> OutPoint {
let txid = Txid::hash(b"some arbitrary bytes");
let vout = 0x15;
OutPoint { txid, vout }
}

/// A dummy `TxOut`, this would usually be the output we are creating with this transaction.
fn dummy_tx_out() -> TxOut {
// Arbitrary script, may not even be a valid scriptPubkey.
let script = ScriptBuf::from_hex("76a914162c5ea71c0b23f5b9022ef047c4a86470a5b07088ac")
.expect("failed to parse script form hex");
let value = Amount::from_sat(123_456_789);
TxOut { value, script_pubkey: script }
}
54 changes: 54 additions & 0 deletions examples/v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! PSBT v2.
//!
//! An example of using PSBT v2 to spend the multisig created in v0-2-of-2-multisig.rs to a p2tr output and a p2wpkh change output.
use psbt::bitcoin::{absolute, OutPoint, Sequence, TxOut};
use psbt::v2::{Constructor, InputBuilder, Modifiable, OutputBuilder};

fn main() -> anyhow::Result<()> {
let previous_output = unspent_multisig_output();
let spend = spend_output();
let change = change_output();

let min_required_height = absolute::Height::from_consensus(800_000).expect("valid height");

// The constructor role.

let constructor = Constructor::<Modifiable>::default();

let input = InputBuilder::new(previous_output)
.minimum_required_height_based_lock_time(min_required_height)
.build();

let psbt = constructor
.input(input)
.output(OutputBuilder::new(spend).build())
.output(OutputBuilder::new(change).build())
.psbt();

// The updater role, should this have the input index?
let _ = psbt.update_sequence(Sequence::ENABLE_LOCKTIME_NO_RBF);

// TODO: Do updates:
// - add inputs[0].witness_utxo
// - add inputs[0].witness_script

// Each party signs a copy of the PSBT.
// let signed_by_a = alice.sign(psbt.clone())?;
// let signed_by_b = bob.sign(psbt)?;

// Combiner combines the two PSBTs (or uses `combine_with` to combine one into the other).
// let signed = psbt::v2::combine(signed_by_a, signed_by_b)?;

// TODO: Finalize and extract.

Ok(())
}

fn unspent_multisig_output() -> OutPoint { todo!() }

/// Spend to a p2tr output.
fn spend_output() -> TxOut { todo!() }

/// Send the change back to us in a p2wpkh output.
fn change_output() -> TxOut { todo!() }
12 changes: 0 additions & 12 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ pub(crate) const PSBT_GLOBAL_UNSIGNED_TX: u8 = 0x00;
/// Type: Extended Public Key PSBT_GLOBAL_XPUB = 0x01
pub(crate) const PSBT_GLOBAL_XPUB: u8 = 0x01;
/// Type: Transaction Version PSBT_GLOBAL_TX_VERSION = 0x02
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_TX_VERSION: u8 = 0x02;
/// Type: Fallback Locktime PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_FALLBACK_LOCKTIME: u8 = 0x03;
/// Type: Input Count PSBT_GLOBAL_INPUT_COUNT = 0x04
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_INPUT_COUNT: u8 = 0x04;
/// Type: Output Count PSBT_GLOBAL_OUTPUT_COUNT = 0x05
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_OUTPUT_COUNT: u8 = 0x05;
/// Type: Transaction Modifiable Flags PSBT_GLOBAL_TX_MODIFIABLE = 0x06
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_GLOBAL_TX_MODIFIABLE: u8 = 0x06;
/// Type: Version Number PSBT_GLOBAL_VERSION = 0xFB
pub(crate) const PSBT_GLOBAL_VERSION: u8 = 0xFB;
Expand Down Expand Up @@ -59,19 +54,14 @@ pub(crate) const PSBT_IN_HASH160: u8 = 0x0c;
/// Type: HASH256 preimage PSBT_IN_HASH256 = 0x0d
pub(crate) const PSBT_IN_HASH256: u8 = 0x0d;
/// Type: Previous TXID PSBT_IN_PREVIOUS_TXID = 0x0e
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_PREVIOUS_TXID: u8 = 0x0e;
/// Type: Spent Output Index PSBT_IN_OUTPUT_INDEX = 0x0f
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_OUTPUT_INDEX: u8 = 0x0f;
/// Type: Sequence Number PSBT_IN_SEQUENCE = 0x10
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_SEQUENCE: u8 = 0x10;
/// Type: Required Time-based Locktime PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_REQUIRED_TIME_LOCKTIME: u8 = 0x11;
/// Type: Required Height-based Locktime PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: u8 = 0x12;
/// Type: Taproot Signature in Key Spend PSBT_IN_TAP_KEY_SIG = 0x13
pub(crate) const PSBT_IN_TAP_KEY_SIG: u8 = 0x13;
Expand All @@ -95,10 +85,8 @@ pub(crate) const PSBT_OUT_WITNESS_SCRIPT: u8 = 0x01;
/// Type: BIP 32 Derivation Path PSBT_OUT_BIP32_DERIVATION = 0x02
pub(crate) const PSBT_OUT_BIP32_DERIVATION: u8 = 0x02;
/// Type: Output Amount PSBT_OUT_AMOUNT = 0x03
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_OUT_AMOUNT: u8 = 0x03;
/// Type: Output Script PSBT_OUT_SCRIPT = 0x04
#[allow(unused)] // PSBT v2
pub(crate) const PSBT_OUT_SCRIPT: u8 = 0x04;
/// Type: Taproot Internal Key PSBT_OUT_TAP_INTERNAL_KEY = 0x05
pub(crate) const PSBT_OUT_TAP_INTERNAL_KEY: u8 = 0x05;
Expand Down
27 changes: 24 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use bitcoin::bip32::Xpub;
// TODO: This should be exposed like this in rust-bitcoin.
use bitcoin::consensus::encode as consensus;
use bitcoin::transaction::Transaction;
use bitcoin::{hashes, secp256k1, taproot};
use bitcoin::{absolute, hashes, secp256k1, taproot};

use crate::prelude::*;
use crate::{io, raw};
use crate::{io, raw, version};

/// Enum for marking psbt hash error.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
Expand Down Expand Up @@ -104,6 +104,12 @@ pub enum Error {
PartialDataConsumption,
/// I/O error.
Io(io::Error),
/// Couldn't converting parsed u32 to a lock time.
LockTime(absolute::Error),
/// Found a keypair type that is explicitly excluded.
ExcludedKey(u8),
/// Unsupported PSBT version.
UnsupportedVersion(version::UnsupportedVersionError),
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -158,6 +164,10 @@ impl fmt::Display for Error {
PartialDataConsumption =>
f.write_str("data not consumed entirely when explicitly deserializing"),
Io(ref e) => write_err!(f, "I/O error"; e),
LockTime(ref e) => write_err!(f, "parsed locktime invalid"; e),
ExcludedKey(t) =>
write!(f, "found a keypair type that is explicitly excluded: {:x}", t),
UnsupportedVersion(ref e) => write_err!(f, "unsupported version"; e),
}
}
}
Expand All @@ -171,6 +181,8 @@ impl std::error::Error for Error {
InvalidHash(ref e) => Some(e),
ConsensusEncoding(ref e) => Some(e),
Io(ref e) => Some(e),
LockTime(ref e) => Some(e),
UnsupportedVersion(ref e) => Some(e),
InvalidMagic
| MissingUtxo
| InvalidSeparator
Expand Down Expand Up @@ -199,7 +211,8 @@ impl std::error::Error for Error {
| TapTree(_)
| XPubKey(_)
| Version(_)
| PartialDataConsumption => None,
| PartialDataConsumption
| ExcludedKey(_) => None,
}
}
}
Expand All @@ -216,6 +229,14 @@ impl From<io::Error> for Error {
fn from(e: io::Error) -> Self { Error::Io(e) }
}

impl From<absolute::Error> for Error {
fn from(e: absolute::Error) -> Self { Error::LockTime(e) }
}

impl From<version::UnsupportedVersionError> for Error {
fn from(e: version::UnsupportedVersionError) -> Self { Error::UnsupportedVersion(e) }
}

/// Formats error.
///
/// If `std` feature is OFF appends error source (delimited by `: `). We do this because
Expand Down
9 changes: 9 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,28 @@ mod sighash_type;
pub mod raw;
pub mod serialize;
pub mod v0;
pub mod v2;
mod version;

#[cfg(feature = "std")]
use std::io;

#[cfg(not(feature = "std"))]
use core2::io;

use crate::version::Version;

#[rustfmt::skip] // Keep pubic re-exports separate
pub use crate::{
error::Error,
sighash_type::PsbtSighashType,
};

/// PSBT version 0 - the original PSBT version.
pub const V0: Version = Version::ZERO;
/// PSBT version 2 - the second PSBT version.
pub const V2: Version = Version::TWO;

#[rustfmt::skip]
mod prelude {
#[cfg(all(not(feature = "std"), not(test)))]
Expand Down
2 changes: 1 addition & 1 deletion src/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
use core::convert::TryFrom;
use core::fmt;

// TODO: This should be exposed like this in rust-bitcoin.
use bitcoin::consensus::encode as consensus;
use bitcoin::consensus::encode::{
deserialize, serialize, Decodable, Encodable, ReadExt, VarInt, WriteExt, MAX_VEC_SIZE,
};
use bitcoin::hex::DisplayHex;

use crate::prelude::*;
use crate::serialize::{Deserialize, Serialize};
Expand Down
56 changes: 55 additions & 1 deletion src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use bitcoin::secp256k1::{self, XOnlyPublicKey};
use bitcoin::taproot::{
ControlBlock, LeafVersion, TapLeafHash, TapNodeHash, TapTree, TaprootBuilder,
};
use bitcoin::{ecdsa, taproot, ScriptBuf, Transaction, TxOut, VarInt, Witness};
use bitcoin::{
absolute, ecdsa, taproot, transaction, Amount, ScriptBuf, Sequence, Transaction, TxOut, Txid,
VarInt, Witness,
};

use crate::prelude::*;
use crate::sighash_type::PsbtSighashType;
Expand All @@ -37,13 +40,18 @@ pub(crate) trait Deserialize: Sized {
fn deserialize(bytes: &[u8]) -> Result<Self, Error>;
}

impl_psbt_de_serialize!(absolute::LockTime);
impl_psbt_de_serialize!(Amount);
impl_psbt_de_serialize!(Transaction);
impl_psbt_de_serialize!(transaction::Version);
impl_psbt_de_serialize!(TxOut);
impl_psbt_de_serialize!(Witness);
impl_psbt_de_serialize!(VarInt);
impl_psbt_hash_de_serialize!(ripemd160::Hash);
impl_psbt_hash_de_serialize!(sha256::Hash);
impl_psbt_hash_de_serialize!(TapLeafHash);
impl_psbt_hash_de_serialize!(TapNodeHash);
impl_psbt_hash_de_serialize!(Txid);
impl_psbt_hash_de_serialize!(hash160::Hash);
impl_psbt_hash_de_serialize!(sha256d::Hash);

Expand Down Expand Up @@ -146,6 +154,52 @@ impl Deserialize for KeySource {
}
}

impl Serialize for u32 {
fn serialize(&self) -> Vec<u8> { serialize(&self) }
}

impl Deserialize for u32 {
fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
let val: u32 = consensus::deserialize(bytes)?;
Ok(val)
}
}

impl Serialize for Sequence {
fn serialize(&self) -> Vec<u8> { serialize(&self) }
}

impl Deserialize for Sequence {
fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
let n: Sequence = consensus::deserialize(bytes)?;
Ok(n)
}
}

impl Serialize for absolute::Height {
fn serialize(&self) -> Vec<u8> { serialize(&self.to_consensus_u32()) }
}

impl Deserialize for absolute::Height {
fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
let n: u32 = consensus::deserialize(bytes)?;
let lock = absolute::Height::from_consensus(n)?;
Ok(lock)
}
}

impl Serialize for absolute::Time {
fn serialize(&self) -> Vec<u8> { serialize(&self.to_consensus_u32()) }
}

impl Deserialize for absolute::Time {
fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
let n: u32 = consensus::deserialize(bytes)?;
let lock = absolute::Time::from_consensus(n)?;
Ok(lock)
}
}

// partial sigs
impl Serialize for Vec<u8> {
fn serialize(&self) -> Vec<u8> { self.clone() }
Expand Down
Loading

0 comments on commit 5e11818

Please sign in to comment.