diff --git a/Cargo.lock b/Cargo.lock index 44f1949205..6f88e07a41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -610,6 +610,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "beef" version = "0.5.2" @@ -672,11 +678,26 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" +dependencies = [ + "bech32", + "bitcoin-private", + "bitcoin_hashes 0.12.0", + "core2 0.3.3", + "hex_lit", + "secp256k1 0.27.0", +] + [[package]] name = "bitcoin" version = "1.2.0" dependencies = [ - "bitcoin_hashes", + "bitcoin 0.30.1", + "bitcoin_hashes 0.7.6", "frame-support", "hex", "impl-serde 0.3.2", @@ -690,12 +711,28 @@ dependencies = [ "spin 0.7.1", ] +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + [[package]] name = "bitcoin_hashes" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b375d62f341cef9cd9e77793ec8f1db3fc9ce2e4d57e982c8fe697a2c16af3b6" +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", + "core2 0.3.3", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -861,7 +898,7 @@ checksum = "bd769563b4ea2953e2825c9e6b7470a5f55f67e0be00030bf3e390a2a6071f64" name = "btc-relay" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "frame-benchmarking", "frame-support", "frame-system", @@ -1085,7 +1122,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ed9c8b2d17acb8110c46f1da5bf4a696d745e1474a16db0cd2b49cd0249bf2" dependencies = [ - "core2", + "core2 0.4.0", "multibase", "multihash 0.16.3", "serde", @@ -1332,6 +1369,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "core2" version = "0.4.0" @@ -4401,6 +4447,12 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hkdf" version = "0.12.3" @@ -4751,7 +4803,7 @@ name = "interbtc-parachain" version = "1.2.0" dependencies = [ "async-trait", - "bitcoin", + "bitcoin 1.2.0", "btc-relay-rpc-runtime-api", "clap", "cumulus-client-cli", @@ -4852,7 +4904,7 @@ dependencies = [ name = "interbtc-primitives" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "bstringify", "parity-scale-codec", "primitive-types", @@ -4930,7 +4982,7 @@ name = "interlay-runtime-parachain" version = "1.2.0" dependencies = [ "annuity", - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "btc-relay-rpc-runtime-api", "clients-info", @@ -5097,7 +5149,7 @@ dependencies = [ name = "issue" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "currency", "fee", @@ -5353,7 +5405,7 @@ name = "kintsugi-runtime-parachain" version = "1.2.0" dependencies = [ "annuity", - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "btc-relay-rpc-runtime-api", "clients-info", @@ -6741,7 +6793,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", - "core2", + "core2 0.4.0", "digest 0.10.7", "multihash-derive", "sha2 0.10.7", @@ -6755,7 +6807,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" dependencies = [ - "core2", + "core2 0.4.0", "multihash-derive", "unsigned-varint", ] @@ -8644,7 +8696,7 @@ name = "parachain-tests" version = "1.2.0" dependencies = [ "annuity", - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "btc-relay-rpc-runtime-api", "clients-info", @@ -10896,7 +10948,7 @@ dependencies = [ name = "redeem" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "currency", "fee", @@ -11069,7 +11121,7 @@ dependencies = [ name = "replace" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "btc-relay", "currency", "fee", @@ -13016,6 +13068,16 @@ dependencies = [ "secp256k1-sys 0.6.1", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes 0.12.0", + "secp256k1-sys 0.8.1", +] + [[package]] name = "secp256k1-sys" version = "0.4.0" @@ -13033,6 +13095,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -15430,7 +15501,7 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" name = "vault-registry" version = "1.2.0" dependencies = [ - "bitcoin", + "bitcoin 1.2.0", "currency", "fee", "fixed-hash 0.7.0", diff --git a/crates/bitcoin/Cargo.toml b/crates/bitcoin/Cargo.toml index da310e6bc8..24ead977cc 100644 --- a/crates/bitcoin/Cargo.toml +++ b/crates/bitcoin/Cargo.toml @@ -15,6 +15,7 @@ spin = { version = "0.7.1", default-features = false } primitive-types = { version = "0.12.1", default-features = false, features = ["codec", "scale-info"] } bitcoin_hashes = { version = "0.7.3", default-features = false } secp256k1 = { package = "secp256k1", git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "8e61874", default-features = false } +rust-bitcoin = { package = "bitcoin", version = "0.30.1", default-features = false, features = ["no-std"], optional = true } [dev-dependencies] mocktopus = "0.8.0" @@ -33,9 +34,11 @@ std = [ "primitive-types/std", "primitive-types/serde", "secp256k1/std", + "rust-bitcoin?/std" ] parser = [] runtime-benchmarks = [] +bitcoin-types-compat = ["rust-bitcoin", "parser"] [[example]] name = "parse-transaction" diff --git a/crates/bitcoin/src/compat.rs b/crates/bitcoin/src/compat.rs new file mode 100644 index 0000000000..940229e3f5 --- /dev/null +++ b/crates/bitcoin/src/compat.rs @@ -0,0 +1,119 @@ +//! Provides conversions between rust-bitcoin and interbtc types. +//! Please note that these operations involve (unbounded) re-encoding +//! and decoding so may be expensive to use. + +use crate::{formatter::TryFormat, parser::Parsable}; +use rust_bitcoin::consensus::{Decodable, Encodable}; + +pub use rust_bitcoin; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +#[derive(Debug)] +pub enum ConversionError { + ParsingError, + FormattingError, +} + +/// Macro to implement type conversion from interbtc types to rust-bitcoin, using consensus encoding +macro_rules! impl_bitcoin_conversion { + ($a:path, $b:path) => { + impl TryFrom<$a> for $b { + type Error = ConversionError; + fn try_from(value: $a) -> Result { + let mut bytes = Vec::::new(); + value + .try_format(&mut bytes) + .map_err(|_| ConversionError::FormattingError)?; + let result = Self::consensus_decode_from_finite_reader(&mut &bytes[..]) + .map_err(|_| ConversionError::ParsingError)?; + Ok(result) + } + } + }; +} + +/// Macro to implement type conversion to interbtc types from rust-bitcoin, using consensus encoding +macro_rules! impl_interbtc_conversion { + ($a:path, $b:path) => { + impl TryFrom<$b> for $a { + type Error = ConversionError; + fn try_from(value: $b) -> Result { + let mut data: Vec = Vec::new(); + value + .consensus_encode(&mut data) + .map_err(|_| ConversionError::FormattingError)?; + let result = Self::parse(&data, 0).map_err(|_| ConversionError::ParsingError)?; + Ok(result.0) + } + } + }; +} + +macro_rules! impl_bidirectional_conversions { + ($a:path, $b:path) => { + impl_bitcoin_conversion!($a, $b); + impl_interbtc_conversion!($a, $b); + }; +} + +// NOTE: rust_bitcoin::Script exists but we can't convert to that because it's unsized +impl_bitcoin_conversion!(crate::Script, rust_bitcoin::ScriptBuf); + +// Transaction conversions +impl_bidirectional_conversions!(crate::types::Transaction, rust_bitcoin::Transaction); + +// Payload -> Address +impl TryFrom for crate::Address { + type Error = ConversionError; + fn try_from(value: rust_bitcoin::address::Payload) -> Result { + let bitcoin_script = value.script_pubkey(); + let bitcoin_script_bytes = bitcoin_script.to_bytes(); + let interlay_script = crate::Script::from(bitcoin_script_bytes); + crate::Address::from_script_pub_key(&interlay_script).map_err(|_| ConversionError::ParsingError) + } +} + +// Address -> Payload +impl TryFrom for rust_bitcoin::address::Payload { + type Error = ConversionError; + fn try_from(value: crate::Address) -> Result { + let interlay_script = value.to_script_pub_key(); + let bitcoin_script = rust_bitcoin::blockdata::script::Script::from_bytes(interlay_script.as_bytes()); + rust_bitcoin::address::Payload::from_script(&bitcoin_script).map_err(|_| ConversionError::ParsingError) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parser::parse_transaction; + + #[test] + fn test_transaction_compat() { + // txid eb3db053cd139147f2fd676cf59a491fd5aebc54bddfde829704585b659126fc + let raw_tx = "0100000000010120e6fb8f0e2cfb8667a140a92d045d5db7c1b56635790bc907c3e71d43720a150e00000017160014641e441c2ba32dd7cf05afde7922144dd106b09bffffffff019dbd54000000000017a914bd847a4912984cf6152547feca51c1b9c2bcbe2787024830450221008f00033064c26cfca4dc98e5dba800b18729c3441dca37b49358ae0df9be7fad02202a81085318466ea66ef390d5dab6737e44a05f7f2e747932ebba917e0098f37d012102c109fc47335c3a2e206d462ad52590b1842aa9d6e0eb9c683c896fa8723590b400000000"; + let tx_bytes = hex::decode(&raw_tx).unwrap(); + let interlay_transaction = parse_transaction(&tx_bytes).unwrap(); + + let rust_bitcoin_transaction: rust_bitcoin::Transaction = interlay_transaction.clone().try_into().unwrap(); + + // check that the rust-bitcoin type encodes to the same bytes + let mut re_encoded_bytes: Vec = Vec::new(); + rust_bitcoin_transaction + .consensus_encode(&mut re_encoded_bytes) + .unwrap(); + assert_eq!(tx_bytes, re_encoded_bytes); + + // check that the conversion back works + assert_eq!(interlay_transaction, rust_bitcoin_transaction.try_into().unwrap()); + } + + #[test] + fn test_address_compat() { + let interbtc_address = crate::Address::P2WPKHv0(primitive_types::H160([1; 20])); + let rust_bitcoin_address: rust_bitcoin::address::Payload = interbtc_address.clone().try_into().unwrap(); + assert_eq!(interbtc_address, rust_bitcoin_address.try_into().unwrap()); + } +} diff --git a/crates/bitcoin/src/lib.rs b/crates/bitcoin/src/lib.rs index 90115bcd7d..e0b8a3b79a 100644 --- a/crates/bitcoin/src/lib.rs +++ b/crates/bitcoin/src/lib.rs @@ -45,6 +45,9 @@ pub mod formatter; #[cfg(any(feature = "parser", test))] pub mod parser; +#[cfg(feature = "bitcoin-types-compat")] +pub mod compat; + pub mod utils; pub mod pow; diff --git a/crates/bitcoin/src/parser.rs b/crates/bitcoin/src/parser.rs index f57345eda2..4fd031e8c0 100644 --- a/crates/bitcoin/src/parser.rs +++ b/crates/bitcoin/src/parser.rs @@ -110,6 +110,68 @@ impl Parsable for Vec { } } +impl Parsable for Transaction { + fn parse(raw_bytes: &[u8], position: usize) -> Result<(Transaction, usize), Error> { + let slice = raw_bytes.get(position..).ok_or(Error::EndOfFile)?; + let mut parser = BytesParser::new(slice); + let version: i32 = parser.parse()?; + + // fail if incorrect version: we only support version 1 and 2 + if version != 1 && version != 2 { + return Err(Error::MalformedTransaction); + } + + let allow_witness = (version & SERIALIZE_TRANSACTION_NO_WITNESS) == 0; + + // TODO: bound maximum? + let mut inputs: Vec = parser.parse_with(version)?; + + let mut flags: u8 = 0; + if inputs.is_empty() && allow_witness { + flags = parser.parse()?; + inputs = parser.parse_with(version)?; + } + + // TODO: bound maximum? + let outputs: Vec = parser.parse()?; + + if (flags & 1) != 0 && allow_witness { + flags ^= 1; + for input in &mut inputs { + input.with_witness(parser.parse()?); + } + + if inputs.iter().all(|input| input.witness.is_empty()) { + // A transaction with a set witness-flag must actually include witnesses in the transaction. + // see https://github.com/bitcoin/bitcoin/blob/be4171679b8eab8205e04ff86140329bd67878a0/src/primitives/transaction.h#L214-L217 + return Err(Error::MalformedTransaction); + } + } + + // https://en.bitcoin.it/wiki/NLockTime + let locktime_or_blockheight: u32 = parser.parse()?; + let lock_at = if locktime_or_blockheight < LOCKTIME_THRESHOLD { + LockTime::BlockHeight(locktime_or_blockheight) + } else { + LockTime::Time(locktime_or_blockheight) + }; + + if flags != 0 { + return Err(Error::MalformedTransaction); + } + + Ok(( + Transaction { + version, + inputs, + outputs, + lock_at, + }, + parser.position, + )) + } +} + impl ParsableMeta for TransactionInput { fn parse_with(raw_bytes: &[u8], position: usize, version: i32) -> Result<(TransactionInput, usize), Error> { let slice = raw_bytes.get(position..).ok_or(Error::EndOfFile)?; @@ -270,59 +332,7 @@ pub fn parse_compact_uint(varint: &[u8]) -> Result<(u64, usize), Error> { /// /// * `raw_transaction` - the raw bytes of the transaction pub fn parse_transaction(raw_transaction: &[u8]) -> Result { - let mut parser = BytesParser::new(raw_transaction); - let version: i32 = parser.parse()?; - - // fail if incorrect version: we only support version 1 and 2 - if version != 1 && version != 2 { - return Err(Error::MalformedTransaction); - } - - let allow_witness = (version & SERIALIZE_TRANSACTION_NO_WITNESS) == 0; - - // TODO: bound maximum? - let mut inputs: Vec = parser.parse_with(version)?; - - let mut flags: u8 = 0; - if inputs.is_empty() && allow_witness { - flags = parser.parse()?; - inputs = parser.parse_with(version)?; - } - - // TODO: bound maximum? - let outputs: Vec = parser.parse()?; - - if (flags & 1) != 0 && allow_witness { - flags ^= 1; - for input in &mut inputs { - input.with_witness(parser.parse()?); - } - - if inputs.iter().all(|input| input.witness.is_empty()) { - // A transaction with a set witness-flag must actually include witnesses in the transaction. - // see https://github.com/bitcoin/bitcoin/blob/be4171679b8eab8205e04ff86140329bd67878a0/src/primitives/transaction.h#L214-L217 - return Err(Error::MalformedTransaction); - } - } - - // https://en.bitcoin.it/wiki/NLockTime - let locktime_or_blockheight: u32 = parser.parse()?; - let lock_at = if locktime_or_blockheight < LOCKTIME_THRESHOLD { - LockTime::BlockHeight(locktime_or_blockheight) - } else { - LockTime::Time(locktime_or_blockheight) - }; - - if flags != 0 { - return Err(Error::MalformedTransaction); - } - - Ok(Transaction { - version, - inputs, - outputs, - lock_at, - }) + Transaction::parse(raw_transaction, 0).map(|(tx, _len)| tx) } /// Parses a transaction input