Skip to content

Commit

Permalink
feat: P2SH (#268)
Browse files Browse the repository at this point in the history
<!-- enter the gh issue after hash -->

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

<!-- PR description below -->

Run the tests using the following command:
```
scarb test -vv -p shinigami_tests -f test_p2sh_transaction_1
```
for 1...5

Test 4 fails to deserialize. Figuring this out

---------

Co-authored-by: Brandon Roberts <brandonjroberts22@gmail.com>
  • Loading branch information
varun-doshi and b-j-roberts authored Oct 29, 2024
1 parent 1a55189 commit 33c8024
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ node_modules/

# Development
**/TODO
.vscode
1 change: 1 addition & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies = [
name = "shinigami_tests"
version = "0.1.0"
dependencies = [
"ripemd160",
"shinigami_compiler",
"shinigami_engine",
"shinigami_utils",
Expand Down
1 change: 1 addition & 0 deletions packages/tests/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024_07"

[dependencies]
ripemd160.workspace = true
shinigami_compiler = { path = "../compiler" }
shinigami_engine = { path = "../engine" }
shinigami_utils = { path = "../utils" }
Expand Down
1 change: 1 addition & 0 deletions packages/tests/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ pub mod tests {
mod test_p2wpkh;
mod test_p2wsh;
mod test_p2ms;
mod test_p2sh;
}
129 changes: 129 additions & 0 deletions packages/tests/src/tests/test_p2sh.cairo

Large diffs are not rendered by default.

48 changes: 48 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,54 @@ fn test_deserialize_first_p2pkh_transaction() {
assert_eq!(transaction.locktime, 0, "Lock time is not correct");
}

#[test]
fn test_deserialize_first_p2sh_transaction() {
//https://learnmeabitcoin.com/explorer/tx/a0f1aaa2fb4582c89e0511df0374a5a2833bf95f7314f4a51b55b7b71e90ce0f
let raw_transaction_hex =
"0x01000000014ce7153d92e3b24d9eea31f8cf391c3fb4c39f7742b341b2d36c6367e7546474000000006c493046022100c554360535b2ad3b1cb1b966a87807f7a7e45fa485348d662a1e7413dced8471022100d6bcfc4385b7ac41ca3968a73c4a28e38879192c3db1286b36e59ec9fce52bbd012103c96e3a9e63986801269d5f278246ed7cdc2d392595d0a25b102e04598f4b4fa9ffffffff02cb871a00000000001976a914c02ebae82202119f23f330781ff26b303edb7dbd88ac809698000000000017a914748284390f9e263a4b766a75d0633c50426eb8758700000000";
let raw_transaction = hex_to_bytecode(@raw_transaction_hex);
let transaction = EngineInternalTransactionTrait::deserialize(raw_transaction);
assert_eq!(transaction.version, 1, "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 = "0x4ce7153d92e3b24d9eea31f8cf391c3fb4c39f7742b341b2d36c6367e7546474";
let expected_txid = hex_to_bytecode(@expected_txid_hex);

let expected_sig_script_hex =
"0x493046022100c554360535b2ad3b1cb1b966a87807f7a7e45fa485348d662a1e7413dced8471022100d6bcfc4385b7ac41ca3968a73c4a28e38879192c3db1286b36e59ec9fce52bbd012103c96e3a9e63986801269d5f278246ed7cdc2d392595d0a25b102e04598f4b4fa9";
let expected_sig_script = hex_to_bytecode(@expected_sig_script_hex);

assert_eq!(
input0.previous_outpoint.txid,
@u256_from_byte_array_with_offset(@expected_txid, 0, 32),
"Outpoint txid on input 2 is not correct"
);
assert_eq!(
input0.signature_script, @expected_sig_script, "Script sig on input 1 is not correct"
);

let output0 = transaction.transaction_outputs[0];
let expected_pk_script_hex_0 = "0x76a914c02ebae82202119f23f330781ff26b303edb7dbd88ac";
let expected_pk_script_0 = hex_to_bytecode(@expected_pk_script_hex_0);

assert_eq!(output0.value, @1738699, "Output 1 value is not correct");
assert_eq!(
output0.publickey_script, @expected_pk_script_0, "Output 1 pk_script is not correct"
);

let output1 = transaction.transaction_outputs[1];
let expected_pk_script_hex_1 = "0xa914748284390f9e263a4b766a75d0633c50426eb87587";
let expected_pk_script_1 = hex_to_bytecode(@expected_pk_script_hex_1);

assert_eq!(output1.value, @10000000, "Output 2 value is not correct");
assert_eq!(
output1.publickey_script, @expected_pk_script_1, "Output 2 pk_script is not correct"
);

assert_eq!(transaction.locktime, 0, "Lock time is not correct");
}

#[test]
fn test_deserialize_p2wsh_transaction() {
// https://learnmeabitcoin.com/explorer/tx/64f427122f7951687aea608b5474509a30616d4e5773a83bc1ed8b8271ad1991
Expand Down
16 changes: 16 additions & 0 deletions packages/tests/src/utils.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,19 @@ pub fn mock_transaction_legacy_sequence_v2(
let outputs = ArrayTrait::<EngineTransactionOutput>::new();
return mock_transaction_with(2, inputs, outputs, 0);
}

//find last push_data opcode in a bytearray
pub fn find_last_index(sig: ByteArray) -> u32 {
let mut i = sig.len() - 1;
loop {
if 1 < sig[i] && sig[i] < 75 {
break;
}
i -= 1;

if (i == 0) {
break;
}
};
return i + 1;
}
67 changes: 67 additions & 0 deletions packages/tests/src/validate.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use shinigami_engine::hash_cache::HashCacheImpl;
use shinigami_engine::transaction::EngineTransaction;
use shinigami_engine::opcodes::Opcode;
use crate::utxo::UTXO;
use crate::utils::find_last_index;
use shinigami_utils::hash::sha256_byte_array;
use shinigami_utils::bytecode::bytecode_to_hex;
use shinigami_utils::bytecode::hex_to_bytecode;
use shinigami_utils::byte_array::sub_byte_array;

// TODO: Move validate coinbase here

Expand Down Expand Up @@ -143,3 +148,65 @@ pub fn validate_p2ms(
Result::Ok(())
}
}


pub fn validate_p2sh(
tx: @EngineTransaction, flags: u32, utxo_hints: Array<UTXO>, indx: u32
) -> Result<(), felt252> {
if tx.transaction_inputs.len() == 0 {
return Result::Err('P2SH: No inputs');
}

let signature_script = bytecode_to_hex(tx.transaction_inputs[indx].signature_script);
let scriptSig_bytes = hex_to_bytecode(@signature_script);

let mut redeem_Script_start_index = 0;
let mut redeem_script_size = 0;
if scriptSig_bytes[0] == 0 || scriptSig_bytes[0] == 1 || scriptSig_bytes[0] == 2 {
//OP_0 OP_PushData <Sig> OP_PushData <RedeemScript> Standard locking scripts
redeem_Script_start_index = (2 + scriptSig_bytes[1] + 1).into();
redeem_script_size = (scriptSig_bytes.len()) - redeem_Script_start_index;
} else {
// non-standard locking script containing a mathematical puzzle
redeem_Script_start_index = find_last_index(scriptSig_bytes.clone());
redeem_script_size = (scriptSig_bytes.len()) - redeem_Script_start_index;
}

let redeem_script = sub_byte_array(
@scriptSig_bytes, ref redeem_Script_start_index, redeem_script_size
);
if redeem_script.len() == 0 {
return Result::Err('P2SH: Redeem Script size = 0');
}
if redeem_script.len() > 520 {
return Result::Err('P2SH: Redeem Script size > 520');
}

let hashed_redeem_script: ByteArray = ripemd160::ripemd160_hash(
@sha256_byte_array(@redeem_script)
)
.into();

let script_pubkey = utxo_hints[0].pubkey_script;
let mut script_hash_start_index = 2;
let script_hash: ByteArray = sub_byte_array(script_pubkey, ref script_hash_start_index, 20);

if hashed_redeem_script != script_hash {
return Result::Err('P2SH: Signature mismatch');
}

let hash_cache = HashCacheImpl::new(tx);
let mut engine = EngineImpl::new(
script_pubkey, tx, indx, flags, *utxo_hints[0].amount, @hash_cache
)
.unwrap();

let res = engine.execute();

if res.is_err() {
let err = res.unwrap_err();
Result::Err(err)
} else {
Result::Ok(())
}
}

0 comments on commit 33c8024

Please sign in to comment.