From b52043ca4c459da8e21384d67c1efebda83b9668 Mon Sep 17 00:00:00 2001 From: okhai <57156589+okhaimie-dev@users.noreply.github.com> Date: Tue, 12 Nov 2024 20:08:30 -0600 Subject: [PATCH] feat: modified bitcoin_transaction_script to infer flags (#278) - [ ] resolves https://github.com/keep-starknet-strange/shinigami/issues/277 - [ ] follows contribution [guide](https://github.com/keep-starknet-strange/shinigami/blob/main/CONTRIBUTING.md) - [ ] code change includes tests --- packages/cmds/src/main.cairo | 18 +++- scripts/get_tx_from_block.sh | 42 +++++++++ scripts/run_bitcoin_transaction.sh | 139 +++++++++++++++++++++-------- 3 files changed, 162 insertions(+), 37 deletions(-) create mode 100755 scripts/get_tx_from_block.sh diff --git a/packages/cmds/src/main.cairo b/packages/cmds/src/main.cairo index 516316eb..8b6c95f8 100644 --- a/packages/cmds/src/main.cairo +++ b/packages/cmds/src/main.cairo @@ -203,14 +203,27 @@ fn backend_debug(input: InputData) -> u8 { #[derive(Drop)] struct ValidateRawInput { raw_transaction: ByteArray, - utxo_hints: Array + utxo_hints: Array, + flags: ByteArray, } fn run_raw_transaction(mut input: ValidateRawInput) -> u8 { println!("Running Bitcoin Script with raw transaction: '{}'", input.raw_transaction); let raw_transaction = hex_to_bytecode(@input.raw_transaction); let transaction = EngineInternalTransactionTrait::deserialize(raw_transaction); + + // Parse the flags + let script_flags = flags::parse_flags(input.flags); + println!("Script flags: {}", script_flags); + + // For coinbase transactions, we expect no UTXO hints since it creates new coins + if input.utxo_hints.is_empty() { + println!("Potential coinbase transaction detected - skipping validation"); + return 1; + } + let mut utxo_hints = array![]; + for hint in input .utxo_hints .span() { @@ -225,7 +238,8 @@ fn run_raw_transaction(mut input: ValidateRawInput) -> u8 { } ); }; - let res = validate::validate_transaction(@transaction, 0, utxo_hints); + + let res = validate::validate_transaction(@transaction, script_flags, utxo_hints); match res { Result::Ok(_) => { println!("Execution successful"); diff --git a/scripts/get_tx_from_block.sh b/scripts/get_tx_from_block.sh new file mode 100755 index 00000000..e0d1a817 --- /dev/null +++ b/scripts/get_tx_from_block.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# +# This script fetches a transaction hash using block height and transaction index +# Usage: ./get_tx_from_block.sh + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + echo "Example: $0 869322 1" + exit 1 +fi + +BLOCK_HEIGHT=$1 +TX_INDEX=$2 + +RPC_API="https://bitcoin-mainnet.public.blastapi.io" + +# First, get the block hash for the given height +BLOCK_HASH_RES=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getblockhash\",\"params\":[$BLOCK_HEIGHT]}" \ + $RPC_API) + +BLOCK_HASH=$(echo $BLOCK_HASH_RES | jq -r '.result') + +if [ -z "$BLOCK_HASH" ] || [ "$BLOCK_HASH" = "null" ]; then + echo "Error: Could not fetch block hash for height $BLOCK_HEIGHT" + exit 1 +fi + +# Then, get the block data which includes all transaction hashes +BLOCK_DATA_RES=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getblock\",\"params\":[\"$BLOCK_HASH\"]}" \ + $RPC_API) + +# Extract the transaction hash at the specified index +TX_HASH=$(echo $BLOCK_DATA_RES | jq -r ".result.tx[$TX_INDEX]") + +if [ -z "$TX_HASH" ] || [ "$TX_HASH" = "null" ]; then + echo "Error: Could not find transaction at index $TX_INDEX in block $BLOCK_HEIGHT" + exit 1 +fi + +echo $TX_HASH diff --git a/scripts/run_bitcoin_transaction.sh b/scripts/run_bitcoin_transaction.sh index 5146eeb8..2b789a4c 100755 --- a/scripts/run_bitcoin_transaction.sh +++ b/scripts/run_bitcoin_transaction.sh @@ -5,6 +5,14 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" BASE_DIR=$SCRIPT_DIR/.. +# BIP activation heights (mainnet) +BIP_16_BLOCK_HEIGHT=173805 # P2SH +BIP_66_BLOCK_HEIGHT=363725 # Strict DER signatures +BIP_65_BLOCK_HEIGHT=388381 # CHECKLOCKTIMEVERIFY +BIP_112_BLOCK_HEIGHT=419328 # CHECKSEQUENCEVERIFY +BIP_141_BLOCK_HEIGHT=481824 # SegWit +BIP_341_BLOCK_HEIGHT=709632 # Taproot + TXID=$1 RPC_API="https://bitcoin-mainnet.public.blastapi.io" @@ -16,8 +24,8 @@ RPC_API="https://bitcoin-mainnet.public.blastapi.io" # pub amount: i64, # pub pubkey_script: ByteArray, # pub block_height: i32, -# // TODO: flags? -#} +# pub flags: ByteArray, +#} # Fetch the transaction RES=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getrawtransaction\",\"params\":[\"$TXID\", true]}" $RPC_API) @@ -34,40 +42,101 @@ RAW_TX_INPUT=$(sed 's/^\[\(.*\)\]$/\1/' <<< $RAW_TX_TEXT) VINS=$(echo $RES | jq '.result.vin') # echo "VINS: $VINS" -BLOCK_HEIGHT=0 # TODO? -AMOUNT=0 # TODO? -UTXOS="" -for vin in $(echo $VINS | jq -r '.[] | @base64'); do - _jq() { - echo ${vin} | base64 --decode | jq -r ${1} - } - - TXID=$(echo $(_jq '.txid')) - VOUT=$(echo $(_jq '.vout')) - - # Fetch the transaction - RES=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getrawtransaction\",\"params\":[\"$TXID\", true]}" $RPC_API) - - # AMOUNT=$(echo $RES | jq ".result.vout[$VOUT].value") - # echo "AMOUNT: $AMOUNT" +# Get block hash from transaction +BLOCK_HASH=$(echo $RES | jq -r '.result.blockhash') +# Fetch block info to get height +BLOCK_INFO=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getblock\",\"params\":[\"$BLOCK_HASH\"]}" $RPC_API) +BLOCK_HEIGHT=$(echo $BLOCK_INFO | jq -r '.result.height') +echo "BLOCK_HEIGHT: $BLOCK_HEIGHT" +BLOCK_VERSION=$(echo $BLOCK_INFO | jq -r '.result.version') + +# Check if this is a coinbase transaction +IS_COINBASE=false +VIN_TXID=$(echo $RES | jq -r '.result.vin[0].txid') +if [ "$VIN_TXID" = "null" ] || [ "$VIN_TXID" = "0000000000000000000000000000000000000000000000000000000000000000" ]; then + IS_COINBASE=true + echo "Detected coinbase transaction" + FLAGS="" # Empty flags for coinbase +else + # Initialize empty flags string + FLAGS="" + + # BIP16 (P2SH) + if [ $BLOCK_HEIGHT -ge $BIP_16_BLOCK_HEIGHT ]; then + FLAGS="P2SH" + fi + + # BIP66 (Strict DER signatures) + if [ $BLOCK_VERSION -ge 3 ] && [ $BLOCK_HEIGHT -ge $BIP_66_BLOCK_HEIGHT ]; then + [ -n "$FLAGS" ] && FLAGS="$FLAGS," + FLAGS="${FLAGS}DERSIG" + fi + + # BIP65 (CHECKLOCKTIMEVERIFY) + if [ $BLOCK_VERSION -ge 4 ] && [ $BLOCK_HEIGHT -ge $BIP_65_BLOCK_HEIGHT ]; then + [ -n "$FLAGS" ] && FLAGS="$FLAGS," + FLAGS="${FLAGS}CHECKLOCKTIMEVERIFY" + fi + + # BIP112 (CHECKSEQUENCEVERIFY) + if [ $BLOCK_HEIGHT -ge $BIP_112_BLOCK_HEIGHT ]; then + [ -n "$FLAGS" ] && FLAGS="$FLAGS," + FLAGS="${FLAGS}CHECKSEQUENCEVERIFY" + fi + + # BIP141 (SegWit) + if [ $BLOCK_HEIGHT -ge $BIP_141_BLOCK_HEIGHT ]; then + [ -n "$FLAGS" ] && FLAGS="$FLAGS," + FLAGS="${FLAGS}WITNESS" + FLAGS="${FLAGS},NULLDUMMY" + fi + + # BIP341 (Taproot) + if [ $BLOCK_HEIGHT -ge $BIP_341_BLOCK_HEIGHT ]; then + [ -n "$FLAGS" ] && FLAGS="$FLAGS," + FLAGS="${FLAGS}TAPROOT" + fi +fi + +echo "Transaction type: $([ "$IS_COINBASE" = true ] && echo 'Coinbase' || echo 'Regular')" +echo "Block height: $BLOCK_HEIGHT" +echo "Script flags: $FLAGS" +echo "UTXO construction: $UTXOS" - PUBKEY_SCRIPT=$(echo $RES | jq ".result.vout[$VOUT].scriptPubKey.hex" | tr -d '"') - PUBKEY_SCRIPT="0x$PUBKEY_SCRIPT" - # echo "PUBKEY_SCRIPT: $PUBKEY_SCRIPT" - - PUBKEY_SCRIPT_TEXT=$($SCRIPT_DIR/text_to_byte_array.sh $PUBKEY_SCRIPT) - PUBKEY_SCRIPT_INPUT=$(sed 's/^\[\(.*\)\]$/\1/' <<< $PUBKEY_SCRIPT_TEXT) - # echo "PUBKEY_SCRIPT_INPUT: $PUBKEY_SCRIPT_INPUT" - - # Construct UTXO - UTXO="{\"amount\":$AMOUNT,\"pubkey_script\":\"$PUBKEY_SCRIPT\",\"block_height\":$BLOCK_HEIGHT}" - # echo "UTXO: $UTXO" - - UTXOS="$UTXOS$AMOUNT,$PUBKEY_SCRIPT_INPUT,$BLOCK_HEIGHT," -done -UTXOS=$(sed 's/,$//' <<< $UTXOS) - -JOINED_INPUT="[$RAW_TX_INPUT,[$UTXOS]]" +AMOUNT=0 # TODO? +if [ "$IS_COINBASE" = true ]; then + echo "Setting up empty UTXO list for coinbase transaction" + # Leave UTXOS empty for coinbase transactions + UTXOS="" +else + UTXOS="" + for vin in $(echo $VINS | jq -r '.[] | @base64'); do + _jq() { + echo ${vin} | base64 --decode | jq -r ${1} + } + + TXID=$(echo $(_jq '.txid')) + VOUT=$(echo $(_jq '.vout')) + + # Fetch the transaction + RES=$(curl -s -X POST -H "Content-Type: application/json" -d "{\"jsonrpc\":\"1.0\",\"id\":0,\"method\":\"getrawtransaction\",\"params\":[\"$TXID\", true]}" $RPC_API) + + AMOUNT=$(echo $RES | jq ".result.vout[$VOUT].value * 100000000 | floor") + PUBKEY_SCRIPT=$(echo $RES | jq ".result.vout[$VOUT].scriptPubKey.hex" | tr -d '"') + PUBKEY_SCRIPT="0x$PUBKEY_SCRIPT" + + PUBKEY_SCRIPT_TEXT=$($SCRIPT_DIR/text_to_byte_array.sh $PUBKEY_SCRIPT) + PUBKEY_SCRIPT_INPUT=$(sed 's/^\[\(.*\)\]$/\1/' <<< $PUBKEY_SCRIPT_TEXT) + + UTXOS="$UTXOS$AMOUNT,$PUBKEY_SCRIPT_INPUT,$BLOCK_HEIGHT" + done + UTXOS=$(sed 's/,$//' <<< $UTXOS) +fi + +FLAGS_TEXT=$($SCRIPT_DIR/text_to_byte_array.sh "$FLAGS") +FLAGS_INPUT=$(sed 's/^\[\(.*\)\]$/\1/' <<< $FLAGS_TEXT) + +JOINED_INPUT="[$RAW_TX_INPUT,[$UTXOS],$FLAGS_INPUT]" # echo "JOINED_INPUT: $JOINED_INPUT" echo "scarb cairo-run --package shinigami_cmds --function run_raw_transaction \"$JOINED_INPUT\""