From 4394272fa5f84ed2be30a262a558e68ff9bca04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kami=C5=84ski?= Date: Fri, 9 Aug 2024 11:23:39 +0200 Subject: [PATCH] Script to generate test data, refactorizations --- Scarb.toml | 2 +- scripts/data/block.jq | 18 ------ scripts/data/block_filter.jq | 72 +++++++++++++++++++++ scripts/data/get_block.sh | 20 ++++++ scripts/data/launch.sh | 67 -------------------- scripts/data/tx_in.jq | 9 --- scripts/data/tx_out.jq | 7 --- src/state.cairo | 24 ++++--- src/validation.cairo | 118 +++++++++++------------------------ tests/blocks/block_0.cairo | 37 +++++++++++ tests/blocks/block_170.cairo | 69 ++++++++++++++++++++ tests/utils.cairo | 22 +++++++ 12 files changed, 275 insertions(+), 190 deletions(-) delete mode 100644 scripts/data/block.jq create mode 100644 scripts/data/block_filter.jq create mode 100755 scripts/data/get_block.sh delete mode 100644 scripts/data/launch.sh delete mode 100644 scripts/data/tx_in.jq delete mode 100644 scripts/data/tx_out.jq create mode 100644 tests/blocks/block_0.cairo create mode 100644 tests/blocks/block_170.cairo create mode 100644 tests/utils.cairo diff --git a/Scarb.toml b/Scarb.toml index 7a78c68a..42e098dc 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2023_11" [scripts] -gen_data_test= "chmod -x ./scripts/data/launch.sh && bash ./scripts/data/launch.sh" +get_block= "./scripts/data/get_block.sh" [dependencies] diff --git a/scripts/data/block.jq b/scripts/data/block.jq deleted file mode 100644 index 2085774b..00000000 --- a/scripts/data/block.jq +++ /dev/null @@ -1,18 +0,0 @@ -def block: -" -use raito::state::{Block, Header, Transaction, TxIn, TxOut}; - -pub fn test_data_block() -> Block { - Block { - header : Header { - version: \(.version), - prev_block_hash: 0x\(.previousblockhash), - merkle_root_hash: 0x\(.merkle_root), - time: \(.timestamp), - bits: \(.bits), - nonce: \(.nonce) - }," - -; - -block diff --git a/scripts/data/block_filter.jq b/scripts/data/block_filter.jq new file mode 100644 index 00000000..dd254f44 --- /dev/null +++ b/scripts/data/block_filter.jq @@ -0,0 +1,72 @@ +def txin_coinbase: + "TxIn { + script: from_base16(\"\(.coinbase)\"), + sequence: \(.sequence), + previous_output: OutPoint { + txid: 0_u256, + vout: 0xffffffff_u32, + txo_index: 0, // TODO: implement + }, + }" +; + +def txin_regular: + "TxIn { + script: from_base16(\"\(.scriptSig.hex)\"), + sequence: \(.sequence), + previous_output: OutPoint { + txid: 0x\(.txid), + vout: \(.vout), + txo_index: 0, // TODO: implement + }, + }" +; + +def txin: + if .coinbase then + txin_coinbase + else + txin_regular + end +; + +def txout: + "TxOut { + value: \(.value*100000000)_u64, + pk_script: from_base16(\"\(.scriptPubKey.hex)\"), + }" +; + +def tx: + "Transaction { + version: \(.version), + is_segwit: false, + inputs: array![\(.vin | map(txin) | join(",\n"))].span(), + outputs: array![\(.vout | map(txout) | join(",\n"))].span(), + lock_time: \(.locktime) + }" +; + + +def block: + "Block { + header : Header { + version: \(.version)_u32, + time: \(.time)_u32, + nonce: \(.nonce)_u32 + }, + txs: array![\(.tx | map(tx) | join(",\n"))].span() + };" +; + +def fixture: +"use super::state::{Block, Header, Transaction, OutPoint, TxIn, TxOut}; + +pub fn block_\(.height)() -> Block { + // block hash: \(.hash) + \( . | block ) +}" +; + +.result | fixture + diff --git a/scripts/data/get_block.sh b/scripts/data/get_block.sh new file mode 100755 index 00000000..f7998ef2 --- /dev/null +++ b/scripts/data/get_block.sh @@ -0,0 +1,20 @@ +#! /usr/bin/env bash +set -e; +set -o pipefail; + +HEIGHT=$(curl -s --user $USERPWD -s -d '{"jsonrpc": "1.0", "id":"curltest", "method": "getblockheader", "params": ["'${1}'"] }' -H 'content-type: text/plain;' $BITCOIN_RPC | jq -r '.result.height') + +curl \ + -s \ + --user $USERPWD \ + -d '{ + "jsonrpc": "1.0", + "id": "curltest", + "method": "getblock", + "params": ["'${1}'", 2] + }' \ + -H 'content-type: text/plain;' $BITCOIN_RPC \ + | jq -r -f scripts/data/block_filter.jq > tests/blocks/block_${HEIGHT}.cairo + + validate_target, validate_timestamp, validate_proof_of_work, compute_block_reward, + compute_total_work, \ No newline at end of file diff --git a/scripts/data/launch.sh b/scripts/data/launch.sh deleted file mode 100644 index af5e894d..00000000 --- a/scripts/data/launch.sh +++ /dev/null @@ -1,67 +0,0 @@ -#! /usr/bin/env bash - -destPath="./src/" -tx_inJqPath="./scripts/data/tx_in.jq" -tx_outJqPath="./scripts/data/tx_out.jq" -blockJqPath="./scripts/data/block.jq" -blockHash="" -api="https://mempool.space/api/block/" - -if [ ! -z "$1" ]; then - blockHash="$1" -else - read -p "Enter bitcoin block hash: " blockHash -fi - -fileName="${destPath}block_$blockHash.cairo" - - -# Recive informations -btcBlock=$(curl -sSL "$api$blockHash") -btcBlockTxs=$(curl -sSL "$api$blockHash/txs") - -#total transactions of the block -tx_count=$(echo $btcBlock | jq -r ".tx_count") -echo "Total of transactions: " $tx_count - -#Put the header block in file -echo $btcBlock | jq -r -f $blockJqPath > $fileName -echo " txs: array![" >> $fileName - -#declare at 1 for skipping the coinbase transaction -idx=1 -total=1 - -#Put transactions in file -while (( $total < $tx_count )); do - if (( $idx % 25 == 0)) then - btcBlockTxs=$(curl -sSL "$api$blockHash/txs/$total") - idx=0 - echo "$total / $tx_count transactions recived" - fi - tx=$(echo $btcBlockTxs | jq -r ".[$idx]") - echo " Transaction {" >> $fileName - echo " version: $(echo $tx | jq -r ".version")," >> $fileName - echo " lock_time: $(echo $tx | jq -r ".locktime")," >> $fileName - echo " inputs: array![" >> $fileName - echo $tx | jq -r ".vin[]" | jq -r -f $tx_inJqPath >> $fileName - echo " ].span()," >> $fileName - echo " outputs: array![" >> $fileName - echo $tx | jq -r ".vout[]" | jq -r -f $tx_outJqPath >> $fileName - echo " ].span()," >> $fileName - echo " }," >> $fileName - ((idx++)) - ((total++)) -done -if (( $total == $tx_count)) then - echo "$total / $tx_count transactions recived" -echo "Execution successful, file created in $fileName" -fi - -#end file -echo " ].span()" >> $fileName -echo " }" >> $fileName -echo "}" >> $fileName -echo "" -echo -e "${green}add: \"pub mod block_$blockHash;\" in lib.cairo${reset}" -echo -e "${green}add: \"use raito::block_$blockHash::test_data_block;\" in your file${reset}" diff --git a/scripts/data/tx_in.jq b/scripts/data/tx_in.jq deleted file mode 100644 index ea31de70..00000000 --- a/scripts/data/tx_in.jq +++ /dev/null @@ -1,9 +0,0 @@ -def tx_in: -" TxIn { - txid: 0x\(.txid), - index: \(.vout), - script: @\"\(.scriptsig)\", - sequence: \(.sequence), - }," -; -tx_in diff --git a/scripts/data/tx_out.jq b/scripts/data/tx_out.jq deleted file mode 100644 index 075d2336..00000000 --- a/scripts/data/tx_out.jq +++ /dev/null @@ -1,7 +0,0 @@ -def tx_out: -" TxOut { - value : \(.value), - pk_script: @\"\(.scriptpubkey)\", - }," -; -tx_out diff --git a/src/state.cairo b/src/state.cairo index 6fcaf700..a24bd8f8 100644 --- a/src/state.cairo +++ b/src/state.cairo @@ -14,7 +14,7 @@ pub struct ChainState { /// Best block. pub best_block_hash: u256, /// Current block. - pub current_target: u32, + pub current_target: u256, /// Start of the current epoch. pub epoch_start_time: u32, /// Previous timestamps. @@ -73,11 +73,11 @@ pub struct Block { pub struct Header { /// The version of the block. pub version: u32, - /// The hash of the previous block in the blockchain. - pub prev_block_hash: u256, /// The timestamp of the block. pub time: u32, /// The difficulty target for mining the block. + /// Not strictly necessary since it can be computed from target + /// But it is cheaper to validate than compute pub bits: u32, /// The nonce used in mining the block. pub nonce: u32, @@ -99,10 +99,6 @@ pub struct Transaction { pub inputs: Span, /// The outputs of the transaction. pub outputs: Span, - /// The list of witnesses, one for each input. - /// Each witness is a list of elements that are to be pushed onto stack. - /// Witnesses do not contribute to TXID but do contribute to wTXID. - pub witnesses: Span>, /// The lock time of the transaction. pub lock_time: u32, } @@ -128,6 +124,20 @@ pub struct TxIn { pub script: @ByteArray, /// The sequence number of the input. pub sequence: u32, + /// The reference to the previous output that is being used as an input. + pub previous_output: OutPoint, + /// The witness data for transactions. + pub witness: Span, +} + + +/// A reference to a transaction output. +#[derive(Drop, Copy)] +pub struct OutPoint { + /// The hash of the referenced transaction. + pub txid: u256, + /// The index of the specific output in the transaction. + pub vout: u32, /// The index of output in the utreexo set (meta field). pub txo_index: u64, } diff --git a/src/validation.cairo b/src/validation.cairo index 3fe41ea7..5cf011d8 100644 --- a/src/validation.cairo +++ b/src/validation.cairo @@ -8,23 +8,22 @@ pub const POW_SATS_AMOUNT: u256 = 8; // Pow to convert in SATS #[generate_trait] impl BlockValidatorImpl of BlockValidator { fn validate_and_apply(self: ChainState, block: Block) -> Result { - validate_prev_block_hash(@self, @block)?; - validate_proof_of_work(@0_u256, @block)?; - validate_target(@self, @block)?; validate_timestamp(@self, @block)?; - let (total_fees, merkle_root) = fee_and_merkle_root(@self, @block)?; + let (total_fees, merkle_root) = fee_and_merkle_root(@block)?; validate_coinbase(@block, total_fees)?; - let best_block_hash = block_hash(@block, merkle_root)?; let prev_timestamps = next_prev_timestamps(@self, @block); let (current_target, epoch_start_time) = adjust_difficulty(@self, @block); - let total_work = compute_total_work( - self.total_work, bits_to_target(block.header.bits).unwrap() - ); + let total_work = compute_total_work(self.total_work, current_target); let block_height = self.block_height + 1; + let best_block_hash = block_hash(@self, @block, merkle_root)?; + + validate_proof_of_work(current_target, best_block_hash)?; + validate_bits(@block, current_target)?; + Result::Ok( ChainState { block_height, @@ -51,37 +50,21 @@ impl TransactionValidatorImpl of TransactionValidator { } } -fn block_hash(block: @Block, merkle_root: u256) -> Result { +fn block_hash(self: @ChainState, block: @Block, merkle_root: u256) -> Result { // TODO: implement Result::Ok(0) } -fn validate_prev_block_hash(self: @ChainState, block: @Block) -> Result<(), ByteArray> { - if self.best_block_hash == block.header.prev_block_hash { - Result::Ok(()) - } else { - Result::Err("Invalid `prev_block_hash`. This block does not extend the current chain.") - } -} - -fn validate_proof_of_work(target: @u256, block: @Block) -> Result<(), ByteArray> { - if block.header.prev_block_hash <= target { +fn validate_proof_of_work(target: u256, block_hash: u256) -> Result<(), ByteArray> { + if block_hash <= target { Result::Ok(()) } else { Result::Err( - "Insufficient proof of work. Expected block hash {block.header.prev_block_hash} to be less than or equal to {target}." + "Insufficient proof of work. Expected block hash {chain_state.best_block_hash} to be less than or equal to {target}." ) } } -fn validate_target(self: @ChainState, block: @Block) -> Result<(), ByteArray> { - if self.current_target == block.header.bits { - Result::Ok(()) - } else { - Result::Err("Target is {block.header.bits}. Expected {self.current_target}") - } -} - fn validate_timestamp(self: @ChainState, block: @Block) -> Result<(), ByteArray> { if block.header.time > (*self.prev_timestamps).at((*self.prev_timestamps).len() - 6) { Result::Ok(()) @@ -107,7 +90,7 @@ fn compute_work_from_target(target: u256) -> u256 { (~target / (target + 1_u256)) + 1_u256 } -fn adjust_difficulty(self: @ChainState, block: @Block) -> (u32, u32) { +fn adjust_difficulty(self: @ChainState, block: @Block) -> (u256, u32) { // TODO: implement (*self.current_target, *self.epoch_start_time) } @@ -118,14 +101,14 @@ fn validate_merkle_root(self: @ChainState, block: @Block) -> Result<(), ByteArra } // Helper functions -pub fn bits_to_target(bits: u32) -> Result { +pub fn bits_to_target(bits: u32) -> Result { // Extract exponent and mantissa let exponent: u32 = (bits / 0x1000000); let mantissa: u32 = bits & 0x00FFFFFF; // Check if mantissa is valid (should be less than 0x1000000) if mantissa > 0x7FFFFF && exponent != 0 { - return Result::Err('Invalid mantissa'); + return Result::Err("Invalid mantissa"); } // Calculate the full target value @@ -145,19 +128,20 @@ pub fn bits_to_target(bits: u32) -> Result { // Ensure the target doesn't exceed the maximum allowed value if target > MAX_TARGET { - return Result::Err('Target exceeds maximum'); + return Result::Err("Target exceeds maximum"); } Result::Ok(target) } -pub fn target_to_bits(target: u256) -> Result { +// TODO: potentially not necessary? +pub fn target_to_bits(target: u256) -> Result { if target == 0 { - return Result::Err('Target is zero'); + return Result::Err("Target is zero"); } if target > MAX_TARGET { - return Result::Err('Exceeds max value'); + return Result::Err("Exceeds max value"); } // Find the most significant byte @@ -183,7 +167,7 @@ pub fn target_to_bits(target: u256) -> Result { // Check size doesn't exceed maximum if size > 34 { - return Result::Err('Overflow'); + return Result::Err("Overflow"); } // Convert size to u256 @@ -195,7 +179,15 @@ pub fn target_to_bits(target: u256) -> Result { Result::Ok(result) } -fn fee_and_merkle_root(self: @ChainState, block: @Block) -> Result<(u256, u256), ByteArray> { +fn validate_bits(block: @Block, target: u256) -> Result<(), ByteArray> { + if *block.header.bits == target_to_bits(target)? { + Result::Ok(()) + } else { + Result::Err("Block header bits do not match target") + } +} + +fn fee_and_merkle_root(block: @Block) -> Result<(u256, u256), ByteArray> { let mut txids = ArrayTrait::new(); let mut total_fee = 0; @@ -224,41 +216,12 @@ fn compute_block_reward(block_height: u32) -> u64 { #[cfg(test)] mod tests { use super::{ - validate_target, validate_timestamp, validate_proof_of_work, compute_block_reward, - compute_total_work, compute_work_from_target, shr, shl, REWARD_INITIAL, POW_SATS_AMOUNT + validate_timestamp, validate_proof_of_work, compute_block_reward, compute_total_work, + compute_work_from_target, shr, shl, REWARD_INITIAL, POW_SATS_AMOUNT }; use super::{Block, ChainState, UtreexoState}; use super::super::state::{Header, Transaction, TxIn, TxOut}; - #[test] - fn test_validate_target() { - let mut chain_state = ChainState { - block_height: 1, - total_work: 1, - best_block_hash: 1, - current_target: 1, - epoch_start_time: 1, - prev_timestamps: array![1, 2, 3, 4, 5].span(), - utreexo_state: UtreexoState { roots: array![].span() }, - }; - let mut block = Block { - header: Header { version: 1, prev_block_hash: 1, time: 1, bits: 1, nonce: 1, }, - txs: ArrayTrait::new().span(), - }; - - let result = validate_target(@chain_state, @block); - assert(result.is_ok(), 'Expected target to be valid'); - - chain_state.current_target = 2; - block.header.bits = 1; - let result = validate_target(@chain_state, @block); - assert(result.is_err(), 'Expected target to be invalid'); - - chain_state.current_target = 1; - block.header.bits = 2; - let result = validate_target(@chain_state, @block); - assert(result.is_err(), 'Expected target to be invalid'); - } #[test] fn test_validate_timestamp() { @@ -272,7 +235,7 @@ mod tests { utreexo_state: UtreexoState { roots: array![].span() }, }; let mut block = Block { - header: Header { version: 1, prev_block_hash: 1, time: 12, bits: 1, nonce: 1, }, + header: Header { version: 1, time: 12, nonce: 1, bits: 1 }, txs: ArrayTrait::new().span(), }; @@ -329,31 +292,24 @@ mod tests { #[test] fn test_validate_proof_of_work() { - let mut block = Block { - header: Header { version: 1, prev_block_hash: 1, time: 12, bits: 1, nonce: 1, }, - txs: ArrayTrait::new().span(), - }; - // target is less than prev block hash - let result = validate_proof_of_work(@0_u256, @block); + let result = validate_proof_of_work(0, 1); assert!(result.is_err(), "Expect target less than prev block hash"); // target is greater than prev block hash - let result = validate_proof_of_work(@2_u256, @block); + let result = validate_proof_of_work(2, 1); assert!(result.is_ok(), "Expect target gt prev block hash"); // target is equal to prev block hash - let result = validate_proof_of_work(@1_u256, @block); + let result = validate_proof_of_work(1, 1); assert!(result.is_ok(), "Expect target equal to prev block hash"); // block prev block hash is greater than target - block.header.prev_block_hash = 2; - let result = validate_proof_of_work(@1_u256, @block); + let result = validate_proof_of_work(1, 2); assert!(result.is_err(), "Expect prev block hash gt target"); // block prev block hash is less than target - block.header.prev_block_hash = 9; - let result = validate_proof_of_work(@10_u256, @block); + let result = validate_proof_of_work(10, 9); assert!(result.is_ok(), "Expect prev block hash lt target"); } diff --git a/tests/blocks/block_0.cairo b/tests/blocks/block_0.cairo new file mode 100644 index 00000000..8dee116e --- /dev/null +++ b/tests/blocks/block_0.cairo @@ -0,0 +1,37 @@ +use super::state::{Block, Header, Transaction, OutPoint, TxIn, TxOut}; + +pub fn block_0() -> Block { + // block hash: 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f + Block { + header: Header { version: 1_u32, time: 1231006505_u32, nonce: 2083236893_u32 }, + txs: array![ + Transaction { + version: 1, + is_segwit: false, + inputs: array![ + TxIn { + script: from_base16( + "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73" + ), + sequence: 4294967295, + previous_output: OutPoint { + txid: 0_u256, vout: 0xffffffff_u32, txo_index: 0, // TODO: implement + }, + } + ] + .span(), + outputs: array![ + TxOut { + value: 5000000000_u64, + pk_script: from_base16( + "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac" + ), + } + ] + .span(), + lock_time: 0 + } + ] + .span() + }; +} diff --git a/tests/blocks/block_170.cairo b/tests/blocks/block_170.cairo new file mode 100644 index 00000000..6a9e7bd7 --- /dev/null +++ b/tests/blocks/block_170.cairo @@ -0,0 +1,69 @@ +use super::state::{Block, Header, Transaction, OutPoint, TxIn, TxOut}; + +pub fn block_170() -> Block { + // block hash: 00000000d1145790a8694403d4063f323d499e655c83426834d4ce2f8dd4a2ee + Block { + header: Header { version: 1_u32, time: 1231731025_u32, nonce: 1889418792_u32 }, + txs: array![ + Transaction { + version: 1, + is_segwit: false, + inputs: array![ + TxIn { + script: from_base16("04ffff001d0102"), + sequence: 4294967295, + previous_output: OutPoint { + txid: 0_u256, vout: 0xffffffff_u32, txo_index: 0, // TODO: implement + }, + } + ] + .span(), + outputs: array![ + TxOut { + value: 5000000000_u64, + pk_script: from_base16( + "4104d46c4968bde02899d2aa0963367c7a6ce34eec332b32e42e5f3407e052d64ac625da6f0718e7b302140434bd725706957c092db53805b821a85b23a7ac61725bac" + ), + } + ] + .span(), + lock_time: 0 + }, + Transaction { + version: 1, + is_segwit: false, + inputs: array![ + TxIn { + script: from_base16( + "47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901" + ), + sequence: 4294967295, + previous_output: OutPoint { + txid: 0x0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9, + vout: 0, + txo_index: 0, // TODO: implement + }, + } + ] + .span(), + outputs: array![ + TxOut { + value: 1000000000_u64, + pk_script: from_base16( + "4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac" + ), + }, + TxOut { + value: 4000000000_u64, + pk_script: from_base16( + "410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac" + ), + } + ] + .span(), + lock_time: 0 + } + ] + .span() + }; +} diff --git a/tests/utils.cairo b/tests/utils.cairo new file mode 100644 index 00000000..79257938 --- /dev/null +++ b/tests/utils.cairo @@ -0,0 +1,22 @@ +fn hex_to_byte(h: u8) -> u8 { + if h >= 48 && h <= 57 { + return h - 48; + } else if h >= 65 && h <= 70 { + return h - 55; + } else if h >= 97 && h <= 102 { + return h - 87; + } + panic!("Wrong hex character: {h}"); + 0 +} + +pub fn from_base16(hexs: @ByteArray) -> ByteArray { + let mut result: ByteArray = Default::default(); + let mut i = 0; + let len = hexs.len(); + while i < len { + result.append_word(hex_to_byte(hexs.at(i).unwrap()).into(), 4); + i += 1; + }; + result +}