Skip to content

Commit

Permalink
WIP: Add PSBT v2
Browse files Browse the repository at this point in the history
  • Loading branch information
tcharding committed Nov 23, 2023
1 parent d68d08c commit 534d94e
Show file tree
Hide file tree
Showing 19 changed files with 4,272 additions and 17 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"
59 changes: 59 additions & 0 deletions examples/v2-separate-creator-constructor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! 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::{absolute, Amount, OutPoint, ScriptBuf, Sequence, 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 {
OutPoint {
txid: Txid::all_zeros(), // Arbitrary obviously invalid txid.
vout: 0,
}
}

/// A dummy `TxOut`, this would usually be the output we are creating with this transaction.
fn dummy_tx_out() -> TxOut {
TxOut { value: Amount::from_sat(100_000), script_pubkey: ScriptBuf::default() }
}
56 changes: 56 additions & 0 deletions examples/v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! 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, Creator, InputBuilder, Modifiable, OutputBuilder};

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

let creator = Creator::new();

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

// The constructor role.

let mut 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.
// 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!() }
5 changes: 0 additions & 5 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,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 Down
10 changes: 9 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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};
Expand Down Expand Up @@ -104,6 +104,8 @@ pub enum Error {
PartialDataConsumption,
/// I/O error.
Io(io::Error),
/// Couldn't converting parsed u32 to a lock time.
LockTime(absolute::Error),
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -158,6 +160,7 @@ 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),
}
}
}
Expand All @@ -171,6 +174,7 @@ 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),
InvalidMagic
| MissingUtxo
| InvalidSeparator
Expand Down Expand Up @@ -216,6 +220,10 @@ 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) }
}

/// Formats error.
///
/// If `std` feature is OFF appends error source (delimited by `: `). We do this because
Expand Down
74 changes: 74 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,93 @@ 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,
};

<<<<<<< HEAD
=======
/// 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;

#[cfg(feature = "base64")]
mod display_from_str {
use core::fmt::{self, Display, Formatter};
use core::str::FromStr;

use bitcoin::base64::display::Base64Display;
use bitcoin::base64::prelude::{Engine as _, BASE64_STANDARD};

use crate::error::write_err;
use crate::Error;

/// Error encountered during PSBT decoding from Base64 string.
#[derive(Debug)]
#[non_exhaustive]
pub enum PsbtParseError {
/// Error in internal PSBT data structure.
PsbtEncoding(Error),
/// Error in PSBT Base64 encoding.
Base64Encoding(bitcoin::base64::DecodeError),
}

impl Display for PsbtParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
use self::PsbtParseError::*;

match *self {
PsbtEncoding(ref e) => write_err!(f, "error in internal PSBT data structure"; e),
Base64Encoding(ref e) => write_err!(f, "error in PSBT base64 encoding"; e),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for PsbtParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use self::PsbtParseError::*;

match self {
PsbtEncoding(e) => Some(e),
Base64Encoding(e) => Some(e),
}
}
}

impl Display for crate::v0::Psbt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", Base64Display::new(&self.serialize(), &BASE64_STANDARD))
}
}

impl FromStr for crate::v0::Psbt {
type Err = PsbtParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let data = BASE64_STANDARD.decode(s).map_err(PsbtParseError::Base64Encoding)?;
crate::v0::Psbt::deserialize(&data).map_err(PsbtParseError::PsbtEncoding)
}
}
}
#[cfg(feature = "base64")]
pub use self::display_from_str::PsbtParseError;

>>>>>>> a33ce62 (WIP: Add PSBT v2)
#[rustfmt::skip]
mod prelude {
#[cfg(all(not(feature = "std"), not(test)))]
Expand Down
51 changes: 50 additions & 1 deletion src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ 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, ScriptBuf, Sequence, Transaction, TxOut, Txid, VarInt, Witness,
};

use crate::prelude::*;
use crate::sighash_type::PsbtSighashType;
Expand All @@ -44,6 +46,7 @@ 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 +149,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
10 changes: 4 additions & 6 deletions src/v0/map/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ use bitcoin::consensus::encode::MAX_VEC_SIZE;
use bitcoin::consensus::Decodable;
use bitcoin::transaction::Transaction;

use crate::consts::{
PSBT_GLOBAL_PROPRIETARY, PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_VERSION, PSBT_GLOBAL_XPUB,
};
use crate::io::{self, Cursor, Read};
use crate::prelude::*;
use crate::v0::map::Map;
use crate::{raw, Error};
use crate::consts::{
PSBT_GLOBAL_UNSIGNED_TX,
PSBT_GLOBAL_XPUB,
PSBT_GLOBAL_VERSION,
PSBT_GLOBAL_PROPRIETARY,
};

/// The global key-value map.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -213,6 +210,7 @@ impl Global {

Ok(())
}

/// Combines this [`Psbt`] with `other` PSBT as described by BIP 174.
///
/// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)`
Expand Down
Loading

0 comments on commit 534d94e

Please sign in to comment.