diff --git a/src/util/psbt/macros.rs b/src/util/psbt/macros.rs new file mode 100644 index 00000000..628d8ba2 --- /dev/null +++ b/src/util/psbt/macros.rs @@ -0,0 +1,40 @@ +macro_rules! impl_psbt_de_serialize { + ($thing:ty) => { + impl_psbt_serialize!($thing); + impl_psbt_deserialize!($thing); + }; +} + +macro_rules! impl_psbt_deserialize { + ($thing:ty) => { + impl ::util::psbt::serialize::Deserialize for $thing { + fn deserialize(bytes: &[u8]) -> Result { + ::consensus::encode::deserialize(&bytes[..]) + } + } + }; +} + +macro_rules! impl_psbt_serialize { + ($thing:ty) => { + impl ::util::psbt::serialize::Serialize for $thing { + fn serialize(&self) -> Vec { + ::consensus::encode::serialize(self) + } + } + }; +} + +macro_rules! impl_psbtmap_consensus_encoding { + ($thing:ty) => { + impl ::consensus::encode::Encodable for $thing { + fn consensus_encode(&self, s: &mut S) -> Result<(), ::consensus::encode::Error> { + for pair in ::util::psbt::Map::get_pairs(self)? { + ::consensus::encode::Encodable::consensus_encode(&pair, s)? + } + + ::consensus::encode::Encodable::consensus_encode(&0x00_u8, s) + } + } + }; +} diff --git a/src/util/psbt/map/global.rs b/src/util/psbt/map/global.rs new file mode 100644 index 00000000..f95f416f --- /dev/null +++ b/src/util/psbt/map/global.rs @@ -0,0 +1,169 @@ +use std::collections::HashMap; +use std::io::Cursor; + +use blockdata::transaction::Transaction; +use consensus::encode::{self, Encodable, Decodable, Decoder}; +use util::psbt::map::Map; +use util::psbt::raw; +use util::psbt; +use util::psbt::Error; + +/// A key-value map for global data. +#[derive(Clone, Debug, PartialEq)] +pub struct Global { + /// The unsigned transaction, scriptSigs and witnesses for each input must be + /// empty. + pub unsigned_tx: Transaction, + /// Unknown global key-value pairs. + pub unknown: HashMap>, +} + +impl Global { + /// Create a Global from an unsigned transaction, error if not unsigned + pub fn from_unsigned_tx(tx: Transaction) -> Result { + for txin in &tx.input { + if !txin.script_sig.is_empty() { + return Err(Error::UnsignedTxHasScriptSigs); + } + + if !txin.witness.is_empty() { + return Err(Error::UnsignedTxHasScriptWitnesses); + } + } + + Ok(Global { + unsigned_tx: tx, + unknown: Default::default(), + }) + } +} + +impl Map for Global { + fn insert_pair(&mut self, pair: raw::Pair) -> Result<(), encode::Error> { + let raw::Pair { + key: raw_key, + value: raw_value, + } = pair; + + match raw_key.type_value { + 0u8 => { + return Err(Error::DuplicateKey(raw_key).into()); + } + _ => { + if self.unknown.contains_key(&raw_key) { + return Err(Error::DuplicateKey(raw_key).into()); + } else { + self.unknown.insert(raw_key, raw_value); + } + } + } + + Ok(()) + } + + fn get_pairs(&self) -> Result, encode::Error> { + let mut rv: Vec = Default::default(); + + rv.push(raw::Pair { + key: raw::Key { + type_value: 0u8, + key: vec![], + }, + value: { + // Manually serialized to ensure 0-input txs are serialized + // without witnesses. + let mut ret = Vec::new(); + self.unsigned_tx.version.consensus_encode(&mut ret)?; + self.unsigned_tx.input.consensus_encode(&mut ret)?; + self.unsigned_tx.output.consensus_encode(&mut ret)?; + self.unsigned_tx.lock_time.consensus_encode(&mut ret)?; + ret + }, + }); + + for (key, value) in self.unknown.iter() { + rv.push(raw::Pair { + key: key.clone(), + value: value.clone(), + }); + } + + Ok(rv) + } + + fn merge(&mut self, other: Self) -> Result<(), psbt::Error> { + if self.unsigned_tx != other.unsigned_tx { + return Err(psbt::Error::UnexpectedUnsignedTx { + expected: self.unsigned_tx.clone(), + actual: other.unsigned_tx, + }); + } + + self.unknown.extend(other.unknown); + Ok(()) + } +} + +impl_psbtmap_consensus_encoding!(Global); + +impl Decodable for Global { + fn consensus_decode(d: &mut D) -> Result { + + let mut tx: Option = None; + let mut unknowns: HashMap> = Default::default(); + + loop { + match raw::Pair::consensus_decode(d) { + Ok(pair) => { + match pair.key.type_value { + 0u8 => { + // key has to be empty + if pair.key.key.is_empty() { + // there can only be one unsigned transaction + if tx.is_none() { + let vlen: usize = pair.value.len(); + let mut decoder = Cursor::new(pair.value); + + // Manually deserialized to ensure 0-input + // txs without witnesses are deserialized + // properly. + tx = Some(Transaction { + version: Decodable::consensus_decode(&mut decoder)?, + input: Decodable::consensus_decode(&mut decoder)?, + output: Decodable::consensus_decode(&mut decoder)?, + lock_time: Decodable::consensus_decode(&mut decoder)?, + }); + + if decoder.position() != vlen as u64 { + return Err(encode::Error::ParseFailed("data not consumed entirely when explicitly deserializing")) + } + } else { + return Err(Error::DuplicateKey(pair.key).into()) + } + } else { + return Err(Error::InvalidKey(pair.key).into()) + } + } + _ => { + if unknowns.contains_key(&pair.key) { + return Err(Error::DuplicateKey(pair.key).into()); + } else { + unknowns.insert(pair.key, pair.value); + } + } + } + } + Err(::consensus::encode::Error::Psbt(::util::psbt::Error::NoMorePairs)) => break, + Err(e) => return Err(e), + } + } + + if let Some(tx) = tx { + let mut rv: Global = Global::from_unsigned_tx(tx)?; + rv.unknown = unknowns; + Ok(rv) + } else { + Err(Error::MustHaveUnsignedTx.into()) + } + } +} diff --git a/src/util/psbt/map/mod.rs b/src/util/psbt/map/mod.rs index 621b865a..b353e427 100644 --- a/src/util/psbt/map/mod.rs +++ b/src/util/psbt/map/mod.rs @@ -13,3 +13,8 @@ pub trait Map { /// Attempt to merge with another key-value map of the same type. fn merge(&mut self, other: Self) -> Result<(), psbt::Error>; } + +// place at end to pick up macros +mod global; + +pub use self::global::Global; diff --git a/src/util/psbt/mod.rs b/src/util/psbt/mod.rs index 7d87d59b..369f1b14 100644 --- a/src/util/psbt/mod.rs +++ b/src/util/psbt/mod.rs @@ -9,14 +9,65 @@ pub use self::error::Error; pub mod raw; +#[macro_use] +mod macros; + +pub mod serialize; + mod map; -pub use self::map::Map; +pub use self::map::{Map, Global}; #[cfg(test)] mod tests { + use bitcoin_hashes::hex::FromHex; + use bitcoin_hashes::sha256d; + + use blockdata::script::Script; + use blockdata::transaction::{Transaction, TxIn, TxOut, OutPoint}; use consensus::encode::{deserialize, serialize}; + use util::psbt::map::Global; use util::psbt::raw; + #[test] + fn serialize_then_deserialize_global() { + let expected = Global { + unsigned_tx: Transaction { + version: 2, + lock_time: 1257139, + input: vec![TxIn { + previous_output: OutPoint { + txid: sha256d::Hash::from_hex( + "f61b1742ca13176464adb3cb66050c00787bb3a4eead37e985f2df1e37718126", + ).unwrap(), + vout: 0, + }, + script_sig: Script::new(), + sequence: 4294967294, + witness: vec![], + }], + output: vec![ + TxOut { + value: 99999699, + script_pubkey: hex_script!( + "76a914d0c59903c5bac2868760e90fd521a4665aa7652088ac" + ), + }, + TxOut { + value: 100000000, + script_pubkey: hex_script!( + "a9143545e6e33b832c47050f24d3eeb93c9c03948bc787" + ), + }, + ], + }, + unknown: Default::default(), + }; + + let actual: Global = deserialize(&serialize(&expected)).unwrap(); + + assert_eq!(expected, actual); + } + #[test] fn serialize_then_deserialize_psbtkvpair() { let expected = raw::Pair { diff --git a/src/util/psbt/serialize.rs b/src/util/psbt/serialize.rs new file mode 100644 index 00000000..165a88b3 --- /dev/null +++ b/src/util/psbt/serialize.rs @@ -0,0 +1,22 @@ +//! # PSBT Serialization +//! +//! Defines traits used for (de)serializing PSBT values into/from raw +//! bytes in PSBT key-value pairs. + +use blockdata::transaction::Transaction; +use consensus::encode; + +/// A trait for serializing a value as raw data for insertion into PSBT +/// key-value pairs. +pub trait Serialize { + /// Serialize a value as raw data. + fn serialize(&self) -> Vec; +} + +/// A trait for deserializing a value from raw data in PSBT key-value pairs. +pub trait Deserialize: Sized { + /// Deserialize a value from raw data. + fn deserialize(bytes: &[u8]) -> Result; +} + +impl_psbt_de_serialize!(Transaction);