Skip to content

Commit

Permalink
feat: add support to deserialize transaction with witness data (#259)
Browse files Browse the repository at this point in the history
<!-- enter the gh issue after hash -->

- [ ] issue #
- [x] follows contribution
[guide](https://github.com/keep-starknet-strange/shinigami/blob/main/CONTRIBUTING.md)
- [x] code change includes tests

<!-- PR description below -->
This PR add support to deserialize transaction with witness data

Co-authored-by: Brandon Roberts <[email protected]>
  • Loading branch information
ptisserand and b-j-roberts authored Oct 16, 2024
1 parent f75c637 commit 5b9f8c9
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 8 deletions.
63 changes: 55 additions & 8 deletions packages/engine/src/transaction.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -211,18 +214,62 @@ pub impl EngineInternalTransactionImpl of EngineInternalTransactionTrait {
outputs.append(output);
i += 1;
};
// TODO: Witness

let mut inputs_with_witness: Array<EngineTransactionInput> = 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<ByteArray> = 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 {
Expand Down
40 changes: 40 additions & 0 deletions packages/tests/src/tests/test_transactions.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 5b9f8c9

Please sign in to comment.