Skip to content

Commit da4f43e

Browse files
committed
feat: basic bitcoin type conversions
1 parent 4c113df commit da4f43e

4 files changed

Lines changed: 180 additions & 53 deletions

File tree

crates/bitcoin/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ spin = { version = "0.7.1", default-features = false }
1515
primitive-types = { version = "0.12.1", default-features = false, features = ["codec", "scale-info"] }
1616
bitcoin_hashes = { version = "0.7.3", default-features = false }
1717
secp256k1 = { package = "secp256k1", git = "https://github.com/rust-bitcoin/rust-secp256k1", rev = "8e61874", default-features = false }
18+
rust-bitcoin = { package = "bitcoin", version = "0.30.1", default-features = false, features = ["no-std"], optional = true }
1819

1920
[dev-dependencies]
2021
mocktopus = "0.8.0"
@@ -33,9 +34,11 @@ std = [
3334
"primitive-types/std",
3435
"primitive-types/serde",
3536
"secp256k1/std",
37+
"rust-bitcoin?/std"
3638
]
3739
parser = []
3840
runtime-benchmarks = []
41+
bitcoin-types-compat = ["rust-bitcoin", "parser"]
3942

4043
[[example]]
4144
name = "parse-transaction"

crates/bitcoin/src/compat.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use crate::{formatter::TryFormat, parser::Parsable};
2+
pub use rust_bitcoin;
3+
use rust_bitcoin::consensus::{Decodable, Encodable};
4+
5+
#[cfg(not(feature = "std"))]
6+
use alloc::vec::Vec;
7+
8+
#[derive(Debug)]
9+
pub enum ConversionError {
10+
ParsingError,
11+
FormattingError,
12+
}
13+
14+
pub trait ConvertFromInterlayBitcoin {
15+
type Output;
16+
fn to_rust_bitcoin(&self) -> Result<Self::Output, ConversionError>;
17+
}
18+
19+
pub trait ConvertToInterlayBitcoin {
20+
type Output;
21+
fn to_interlay(&self) -> Result<Self::Output, ConversionError>;
22+
}
23+
24+
/// Macro to implement type conversion from interlay type to rust-bitcoin, using consensus encoding
25+
macro_rules! impl_bitcoin_conversion {
26+
($a:path, $b:path) => {
27+
impl ConvertFromInterlayBitcoin for $a {
28+
type Output = $b;
29+
fn to_rust_bitcoin(&self) -> Result<Self::Output, ConversionError> {
30+
let mut bytes = Vec::<u8>::new();
31+
self.try_format(&mut bytes)
32+
.map_err(|_| ConversionError::FormattingError)?;
33+
34+
let result = Self::Output::consensus_decode_from_finite_reader(&mut &bytes[..])
35+
.map_err(|_| ConversionError::ParsingError)?;
36+
37+
Ok(result)
38+
}
39+
}
40+
};
41+
}
42+
/// Macro to implement type conversion to interlay type from rust-bitcoin, using consensus encoding
43+
macro_rules! impl_to_interlay_bitcoin_conversion {
44+
($a:path, $b:path) => {
45+
impl ConvertToInterlayBitcoin for $b {
46+
type Output = $a;
47+
fn to_interlay(&self) -> Result<Self::Output, ConversionError> {
48+
let mut data: Vec<u8> = Vec::new();
49+
self.consensus_encode(&mut data)
50+
.map_err(|_| ConversionError::FormattingError)?;
51+
let result = Self::Output::parse(&data, 0).map_err(|_| ConversionError::ParsingError)?;
52+
Ok(result.0)
53+
}
54+
}
55+
};
56+
}
57+
58+
macro_rules! impl_bidirectional_bitcoin_conversion {
59+
($a:path, $b:path) => {
60+
impl_bitcoin_conversion!($a, $b);
61+
impl_to_interlay_bitcoin_conversion!($a, $b);
62+
};
63+
}
64+
65+
// there also exists rust_bitcoin::Script but we can't convert to that since it's unsized
66+
impl_bitcoin_conversion!(crate::Script, rust_bitcoin::ScriptBuf);
67+
68+
// Transcation conversions
69+
impl_bidirectional_bitcoin_conversion!(crate::types::Transaction, rust_bitcoin::Transaction);
70+
71+
// Address <--> Payload
72+
impl ConvertToInterlayBitcoin for rust_bitcoin::address::Payload {
73+
type Output = crate::Address;
74+
fn to_interlay(&self) -> Result<Self::Output, ConversionError> {
75+
let bitcoin_script = self.script_pubkey();
76+
let bitcoin_script_bytes = bitcoin_script.to_bytes();
77+
let interlay_script = crate::Script::from(bitcoin_script_bytes);
78+
Ok(crate::Address::from_script_pub_key(&interlay_script).map_err(|_| ConversionError::ParsingError)?)
79+
}
80+
}
81+
impl ConvertFromInterlayBitcoin for crate::Address {
82+
type Output = rust_bitcoin::address::Payload;
83+
fn to_rust_bitcoin(&self) -> Result<Self::Output, ConversionError> {
84+
let interlay_script = self.to_script_pub_key();
85+
let bitcoin_script = rust_bitcoin::blockdata::script::Script::from_bytes(interlay_script.as_bytes());
86+
Ok(rust_bitcoin::address::Payload::from_script(&bitcoin_script).map_err(|_| ConversionError::ParsingError)?)
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
use crate::parser::parse_transaction;
94+
#[test]
95+
fn test_bitcoin_compat() {
96+
// txid eb3db053cd139147f2fd676cf59a491fd5aebc54bddfde829704585b659126fc
97+
let raw_tx = "0100000000010120e6fb8f0e2cfb8667a140a92d045d5db7c1b56635790bc907c3e71d43720a150e00000017160014641e441c2ba32dd7cf05afde7922144dd106b09bffffffff019dbd54000000000017a914bd847a4912984cf6152547feca51c1b9c2bcbe2787024830450221008f00033064c26cfca4dc98e5dba800b18729c3441dca37b49358ae0df9be7fad02202a81085318466ea66ef390d5dab6737e44a05f7f2e747932ebba917e0098f37d012102c109fc47335c3a2e206d462ad52590b1842aa9d6e0eb9c683c896fa8723590b400000000";
98+
let tx_bytes = hex::decode(&raw_tx).unwrap();
99+
let interlay_transaction = parse_transaction(&tx_bytes).unwrap();
100+
101+
let rust_bitcoin_transaction = interlay_transaction.to_rust_bitcoin().unwrap();
102+
103+
// check that the rust-bitcoin type encoded to the same bytes
104+
let mut reencoded_bytes: Vec<u8> = Vec::new();
105+
rust_bitcoin_transaction.consensus_encode(&mut reencoded_bytes).unwrap();
106+
assert_eq!(tx_bytes, reencoded_bytes);
107+
108+
// check that the conversion back works
109+
assert_eq!(interlay_transaction, rust_bitcoin_transaction.to_interlay().unwrap());
110+
}
111+
}

crates/bitcoin/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub mod formatter;
4545
#[cfg(any(feature = "parser", test))]
4646
pub mod parser;
4747

48+
#[cfg(feature = "bitcoin-types-compat")]
49+
pub mod compat;
50+
4851
pub mod utils;
4952

5053
pub mod pow;

crates/bitcoin/src/parser.rs

Lines changed: 63 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,68 @@ impl Parsable for Vec<bool> {
110110
}
111111
}
112112

113+
impl Parsable for Transaction {
114+
fn parse(raw_bytes: &[u8], position: usize) -> Result<(Transaction, usize), Error> {
115+
let slice = raw_bytes.get(position..).ok_or(Error::EndOfFile)?;
116+
let mut parser = BytesParser::new(slice);
117+
let version: i32 = parser.parse()?;
118+
119+
// fail if incorrect version: we only support version 1 and 2
120+
if version != 1 && version != 2 {
121+
return Err(Error::MalformedTransaction);
122+
}
123+
124+
let allow_witness = (version & SERIALIZE_TRANSACTION_NO_WITNESS) == 0;
125+
126+
// TODO: bound maximum?
127+
let mut inputs: Vec<TransactionInput> = parser.parse_with(version)?;
128+
129+
let mut flags: u8 = 0;
130+
if inputs.is_empty() && allow_witness {
131+
flags = parser.parse()?;
132+
inputs = parser.parse_with(version)?;
133+
}
134+
135+
// TODO: bound maximum?
136+
let outputs: Vec<TransactionOutput> = parser.parse()?;
137+
138+
if (flags & 1) != 0 && allow_witness {
139+
flags ^= 1;
140+
for input in &mut inputs {
141+
input.with_witness(parser.parse()?);
142+
}
143+
144+
if inputs.iter().all(|input| input.witness.is_empty()) {
145+
// A transaction with a set witness-flag must actually include witnesses in the transaction.
146+
// see https://github.com/bitcoin/bitcoin/blob/be4171679b8eab8205e04ff86140329bd67878a0/src/primitives/transaction.h#L214-L217
147+
return Err(Error::MalformedTransaction);
148+
}
149+
}
150+
151+
// https://en.bitcoin.it/wiki/NLockTime
152+
let locktime_or_blockheight: u32 = parser.parse()?;
153+
let lock_at = if locktime_or_blockheight < LOCKTIME_THRESHOLD {
154+
LockTime::BlockHeight(locktime_or_blockheight)
155+
} else {
156+
LockTime::Time(locktime_or_blockheight)
157+
};
158+
159+
if flags != 0 {
160+
return Err(Error::MalformedTransaction);
161+
}
162+
163+
Ok((
164+
Transaction {
165+
version,
166+
inputs,
167+
outputs,
168+
lock_at,
169+
},
170+
parser.position,
171+
))
172+
}
173+
}
174+
113175
impl ParsableMeta<i32> for TransactionInput {
114176
fn parse_with(raw_bytes: &[u8], position: usize, version: i32) -> Result<(TransactionInput, usize), Error> {
115177
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> {
270332
///
271333
/// * `raw_transaction` - the raw bytes of the transaction
272334
pub fn parse_transaction(raw_transaction: &[u8]) -> Result<Transaction, Error> {
273-
let mut parser = BytesParser::new(raw_transaction);
274-
let version: i32 = parser.parse()?;
275-
276-
// fail if incorrect version: we only support version 1 and 2
277-
if version != 1 && version != 2 {
278-
return Err(Error::MalformedTransaction);
279-
}
280-
281-
let allow_witness = (version & SERIALIZE_TRANSACTION_NO_WITNESS) == 0;
282-
283-
// TODO: bound maximum?
284-
let mut inputs: Vec<TransactionInput> = parser.parse_with(version)?;
285-
286-
let mut flags: u8 = 0;
287-
if inputs.is_empty() && allow_witness {
288-
flags = parser.parse()?;
289-
inputs = parser.parse_with(version)?;
290-
}
291-
292-
// TODO: bound maximum?
293-
let outputs: Vec<TransactionOutput> = parser.parse()?;
294-
295-
if (flags & 1) != 0 && allow_witness {
296-
flags ^= 1;
297-
for input in &mut inputs {
298-
input.with_witness(parser.parse()?);
299-
}
300-
301-
if inputs.iter().all(|input| input.witness.is_empty()) {
302-
// A transaction with a set witness-flag must actually include witnesses in the transaction.
303-
// see https://github.com/bitcoin/bitcoin/blob/be4171679b8eab8205e04ff86140329bd67878a0/src/primitives/transaction.h#L214-L217
304-
return Err(Error::MalformedTransaction);
305-
}
306-
}
307-
308-
// https://en.bitcoin.it/wiki/NLockTime
309-
let locktime_or_blockheight: u32 = parser.parse()?;
310-
let lock_at = if locktime_or_blockheight < LOCKTIME_THRESHOLD {
311-
LockTime::BlockHeight(locktime_or_blockheight)
312-
} else {
313-
LockTime::Time(locktime_or_blockheight)
314-
};
315-
316-
if flags != 0 {
317-
return Err(Error::MalformedTransaction);
318-
}
319-
320-
Ok(Transaction {
321-
version,
322-
inputs,
323-
outputs,
324-
lock_at,
325-
})
335+
Transaction::parse(raw_transaction, 0).map(|(tx, _len)| tx)
326336
}
327337

328338
/// Parses a transaction input

0 commit comments

Comments
 (0)