From 8f166a814d0ee72cc8efebd5d91b917a62e58a27 Mon Sep 17 00:00:00 2001
From: Patrice Tisserand
Date: Tue, 15 Oct 2024 16:34:29 +0200
Subject: [PATCH] feat: add support to deserialize transaction with witness
data
---
.../engine/src/tests/test_transactions.cairo | 40 ++++++++++++
packages/engine/src/transaction.cairo | 62 ++++++++++++++++---
2 files changed, 94 insertions(+), 8 deletions(-)
diff --git a/packages/engine/src/tests/test_transactions.cairo b/packages/engine/src/tests/test_transactions.cairo
index ed9db51f..06fb443f 100644
--- a/packages/engine/src/tests/test_transactions.cairo
+++ b/packages/engine/src/tests/test_transactions.cairo
@@ -121,6 +121,46 @@ fn test_deserialize_first_p2pkh_transaction() {
assert_eq!(transaction.locktime, 0, "Lock time is not correct");
}
+#[test]
+fn test_deserialize_p2wsh_transaction() {
+ // https://learnmeabitcoin.com/explorer/tx/64f427122f7951687aea608b5474509a30616d4e5773a83bc1ed8b8271ad1991
+ let raw_transaction_hex =
+ "0x020000000001018a39b5cdd48c7d45a31a89cd675a95f5de78aebeeda1e55ac35d7110c3bacfc60000000000ffffffff01204e0000000000001976a914ee63c8c790952de677d1f8019c9474d84098d6e188ac0202123423aa20a23421f2ba909c885a3077bb6f8eb4312487797693bbcfe7e311f797e3c5b8fa8700000000";
+ let raw_transaction = hex_to_bytecode(@raw_transaction_hex);
+ let transaction = TransactionTrait::deserialize(raw_transaction);
+
+ assert_eq!(transaction.version, 2, "Version is not correct");
+ assert_eq!(transaction.transaction_inputs.len(), 1, "Transaction inputs length is not correct");
+ let input0 = transaction.transaction_inputs[0];
+ let expected_txid_hex = "0x8a39b5cdd48c7d45a31a89cd675a95f5de78aebeeda1e55ac35d7110c3bacfc6";
+ let expected_txid = hex_to_bytecode(@expected_txid_hex);
+ let expected_witness_1_hex = "0x1234";
+ let expected_witness_1 = hex_to_bytecode(@expected_witness_1_hex);
+ let expected_witness_2_hex =
+ "0xaa20a23421f2ba909c885a3077bb6f8eb4312487797693bbcfe7e311f797e3c5b8fa87";
+ let expected_witness_2 = hex_to_bytecode(@expected_witness_2_hex);
+
+ assert_eq!(input0.previous_outpoint.vout, @0, "Outpoint vout on input 1 is not correct");
+ assert_eq!(
+ input0.previous_outpoint.txid,
+ @u256_from_byte_array_with_offset(@expected_txid, 0, 32),
+ "Outpoint txid on input 1 is not correct"
+ );
+ assert_eq!(input0.signature_script.len(), 0, "Script sig on input 1 is not empty");
+ assert_eq!(input0.witness.len(), 2, "Witness length on input 1 is not correct");
+ assert_eq!(input0.witness[0], @expected_witness_1, "Witness 1 on input 1 is not correct");
+ assert_eq!(input0.witness[1], @expected_witness_2, "Witness 2 on input 1 is not correct");
+ assert_eq!(input0.sequence, @0xFFFFFFFF, "Sequence on input 1 is not correct");
+
+ let output0 = transaction.transaction_outputs[0];
+ assert_eq!(output0.value, @20000, "Output 1 value is not correct");
+ let expected_pk_script_hex = "0x76a914ee63c8c790952de677d1f8019c9474d84098d6e188ac";
+ let expected_pk_script = hex_to_bytecode(@expected_pk_script_hex);
+ assert_eq!(output0.publickey_script, @expected_pk_script, "Output 1 pk_script is not correct");
+
+ assert_eq!(transaction.locktime, 0, "Lock time is not correct");
+}
+
#[test]
fn test_deserialize_coinbase_transaction() { // TODO
}
diff --git a/packages/engine/src/transaction.cairo b/packages/engine/src/transaction.cairo
index e6d87730..a817c297 100644
--- a/packages/engine/src/transaction.cairo
+++ b/packages/engine/src/transaction.cairo
@@ -112,9 +112,12 @@ pub impl TransactionImpl of TransactionTrait {
fn btc_decode(raw: ByteArray, encoding: u32) -> Transaction {
let mut offset: usize = 0;
let version: i32 = byte_array_value_at_le(@raw, ref offset, 4).try_into().unwrap();
+ if encoding == WITNESS_ENCODING {
+ // consume flags
+ offset += 2;
+ }
// TODO: ReadVerIntBuf
let input_len: u8 = byte_array_value_at_le(@raw, ref offset, 1).try_into().unwrap();
- // TODO: input_len = 0 -> segwit
// TODO: Error handling and bounds checks
// TODO: Byte orderings
let mut i = 0;
@@ -150,18 +153,61 @@ pub impl TransactionImpl of TransactionTrait {
outputs.append(output);
i += 1;
};
- // TODO: Witness
+
+ let mut inputs_with_witness: Array = array![];
+
+ if encoding == WITNESS_ENCODING {
+ // one witness for each input
+ i = 0;
+ while i != input_len {
+ let witness_count: u8 = byte_array_value_at_le(@raw, ref offset, 1)
+ .try_into()
+ .unwrap();
+ let mut j = 0;
+ let mut witness: Array = array![];
+ while j != witness_count {
+ let script_len = byte_array_value_at_le(@raw, ref offset, 1)
+ .try_into()
+ .unwrap();
+ let script = sub_byte_array(@raw, ref offset, script_len);
+ witness.append(script);
+ j += 1;
+ };
+ // update Transaction Input
+ let input = inputs.at(i.into());
+ let mut input_with_witness = input.clone();
+ input_with_witness.witness = witness;
+ inputs_with_witness.append(input_with_witness);
+ i += 1;
+ };
+ }
let locktime: u32 = byte_array_value_at_le(@raw, ref offset, 4).try_into().unwrap();
- Transaction {
- version: version,
- transaction_inputs: inputs,
- transaction_outputs: outputs,
- locktime: locktime,
+ if encoding == WITNESS_ENCODING {
+ Transaction {
+ version: version,
+ transaction_inputs: inputs_with_witness,
+ transaction_outputs: outputs,
+ locktime: locktime,
+ }
+ } else {
+ Transaction {
+ version: version,
+ transaction_inputs: inputs,
+ transaction_outputs: outputs,
+ locktime: locktime,
+ }
}
}
fn deserialize(raw: ByteArray) -> Transaction {
- Self::btc_decode(raw, WITNESS_ENCODING)
+ let mut offset: usize = 0;
+ let _version: i32 = byte_array_value_at_le(@raw, ref offset, 4).try_into().unwrap();
+ let flags: u16 = byte_array_value_at_le(@raw, ref offset, 2).try_into().unwrap();
+ if flags == 0x100 {
+ Self::btc_decode(raw, WITNESS_ENCODING)
+ } else {
+ Self::btc_decode(raw, BASE_ENCODING)
+ }
}
fn deserialize_no_witness(raw: ByteArray) -> Transaction {