From 5b9f8c96286082ab6920ad7dfcce9fcdb15bc364 Mon Sep 17 00:00:00 2001 From: ptisserand Date: Thu, 17 Oct 2024 01:24:41 +0200 Subject: [PATCH] feat: add support to deserialize transaction with witness data (#259) - [ ] issue # - [x] follows contribution [guide](https://github.com/keep-starknet-strange/shinigami/blob/main/CONTRIBUTING.md) - [x] code change includes tests This PR add support to deserialize transaction with witness data Co-authored-by: Brandon Roberts --- packages/engine/src/transaction.cairo | 63 ++++++++++++++++--- .../tests/src/tests/test_transactions.cairo | 40 ++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/packages/engine/src/transaction.cairo b/packages/engine/src/transaction.cairo index 5dfeeb1..6d5562e 100644 --- a/packages/engine/src/transaction.cairo +++ b/packages/engine/src/transaction.cairo @@ -173,9 +173,12 @@ pub impl EngineInternalTransactionImpl of EngineInternalTransactionTrait { fn btc_decode(raw: ByteArray, encoding: u32) -> EngineTransaction { 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; @@ -211,18 +214,62 @@ pub impl EngineInternalTransactionImpl of EngineInternalTransactionTrait { 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(); - EngineTransaction { - version: version, - transaction_inputs: inputs, - transaction_outputs: outputs, - locktime: locktime, + + if encoding == WITNESS_ENCODING { + EngineTransaction { + version: version, + transaction_inputs: inputs_with_witness, + transaction_outputs: outputs, + locktime: locktime, + } + } else { + EngineTransaction { + version: version, + transaction_inputs: inputs, + transaction_outputs: outputs, + locktime: locktime, + } } } fn deserialize(raw: ByteArray) -> EngineTransaction { - 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) -> EngineTransaction { diff --git a/packages/tests/src/tests/test_transactions.cairo b/packages/tests/src/tests/test_transactions.cairo index 996b9e1..31003cb 100644 --- a/packages/tests/src/tests/test_transactions.cairo +++ b/packages/tests/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 = EngineInternalTransactionTrait::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 }