diff --git a/scripts/data/format_args.py b/scripts/data/format_args.py index 188f3fa8..baff5b81 100644 --- a/scripts/data/format_args.py +++ b/scripts/data/format_args.py @@ -13,7 +13,7 @@ def serialize(obj): dec string (0-9) -> (int, int) -> u256 = { lo: felt252, hi: felt252 } hex string (0-F), 64 len -> (int, int, int, int, int, int, int, int) -> Hash !reversed! hex string 0x prefixed -> ([int, ...], int, int) -> ByteArray - list -> [] + list -> tuple(len(list), *list) dict -> tuple(dict.values) """ if isinstance(obj, bool): @@ -23,7 +23,11 @@ def serialize(obj): assert(obj >= 0 and obj < 2 ** 252) return obj elif isinstance(obj, str): - if obj.isdigit(): + if obj == "0" * 64: + # special case - zero hash + return (0, 0, 0, 0, 0, 0, 0, 0) + elif obj.isdigit(): + # TODO: there might still be collisions with hashes # Try to cast to int and then to low/high parts num = int(obj) assert(num >= 0 and num < 2 ** 256) @@ -39,14 +43,15 @@ def serialize(obj): main = [int.from_bytes(src[i:i+31], 'big') for i in range(0, main_len, 31)] # TODO: check if this is how byte31 is implemented rem = int.from_bytes(src[main_len:].rjust(31, b'\x00'), 'big') - return (main, rem, rem_len) + return tuple([len(main)] + main + [rem, rem_len]) else: # Reversed hex string into 4-byte words then into BE u32 assert(len(obj) == 64) rev = list(reversed(bytes.fromhex(obj))) return tuple(int.from_bytes(rev[i:i+4], 'big') for i in range(0, 32, 4)) elif isinstance(obj, list): - return list(map(serialize, obj)) + arr = list(map(serialize, obj)) + return tuple([len(arr)] + arr) elif isinstance(obj, dict): return tuple(map(serialize, obj.values())) elif isinstance(obj, tuple): @@ -105,7 +110,7 @@ def format_args(): raise TypeError("Expected single argument") args = json.loads(Path(sys.argv[1]).read_text()) res = flatten_tuples(serialize(args)) - print(res) + print([res]) if __name__ == '__main__': diff --git a/scripts/data/generate_data.py b/scripts/data/generate_data.py index 400f86ef..7338b6cc 100755 --- a/scripts/data/generate_data.py +++ b/scripts/data/generate_data.py @@ -136,9 +136,9 @@ def resolve_transaction(transaction: dict): "version": transaction['version'], # Skip the first 4 bytes (version) and take the next 4 bytes (marker + flag) "is_segwit": transaction["hex"][8:12] == "0001", - "lock_time": transaction['locktime'], "inputs": [resolve_input(input) for input in transaction['vin']], "outputs": [format_output(output) for output in transaction['vout']], + "lock_time": transaction['locktime'], } @@ -151,8 +151,8 @@ def resolve_input(input: dict): return { "script": f'0x{input["scriptSig"]["hex"]}', "sequence": input['sequence'], - "witness": [f'0x{item}' for item in input.get('txinwitness', [])], "previous_output": resolve_outpoint(input), + "witness": [f'0x{item}' for item in input.get('txinwitness', [])], } diff --git a/scripts/data/integration_tests.sh b/scripts/data/integration_tests.sh index 4d410313..4feba3f2 100755 --- a/scripts/data/integration_tests.sh +++ b/scripts/data/integration_tests.sh @@ -17,7 +17,6 @@ test_files="tests/data"/* ignored_files=( "tests/data/light_481823.json" "tests/data/light_709631.json" - "tests/data/full_169.json" ) ignored="${ignored_files[@]}" diff --git a/src/main.cairo b/src/main.cairo index fd4cf98e..ab583395 100644 --- a/src/main.cairo +++ b/src/main.cairo @@ -1,13 +1,25 @@ use crate::types::block::Block; use crate::types::chain_state::{ChainState, BlockValidator}; +/// Raito program arguments. +#[derive(Serde)] +struct Args { + /// Current (initial) chain state + chain_state: ChainState, + /// Batch of blocks that have to be applied to the current chain state + blocks: Array, +} + /// Raito program entrypoint. /// /// Receives current chain state and pending blocks, /// then validates and applies them one by one. /// Returns new chain state in case of succes, otherwise raises an error. -fn main(mut chain_state: ChainState, mut blocks: Array) -> ChainState { - while let Option::Some(block) = blocks.pop_front() { +fn main(mut arguments: Span) -> ChainState { + let Args { mut chain_state, blocks, } = Serde::::deserialize(ref arguments) + .expect('Failed to deserialize'); + + for block in blocks { chain_state = chain_state.validate_and_apply(block).expect('Validation failed'); }; chain_state diff --git a/src/test.cairo b/src/test.cairo index 46bc53c7..0194aa8b 100644 --- a/src/test.cairo +++ b/src/test.cairo @@ -2,15 +2,31 @@ use crate::types::block::Block; use crate::types::chain_state::{ChainState, BlockValidator}; use core::testing::get_available_gas; +/// Integration testing program arguments. +#[derive(Serde)] +struct Args { + /// Current (initial) chain state + chain_state: ChainState, + /// Batch of blocks that have to be applied to the current chain state + blocks: Array, + /// Expected chain state (that we want to compare the result with) + expected_chain_state: ChainState, +} + /// Integration testing program entrypoint. /// -/// Receives current chain state, pending blocks, and expected result. -/// Validates and applies blocks one by one, exits on first failure. -fn test( - mut chain_state: ChainState, mut blocks: Array, mut expected_chain_state: ChainState -) { +/// Receives arguments in a serialized format (Cairo serde). +/// Panics in case of a validation error or chain state mismatch. +/// Prints result to the stdout. +fn test(mut arguments: Span) { + let Args { mut chain_state, blocks, expected_chain_state } = Serde::< + Args + >::deserialize(ref arguments) + .expect('Failed to deserialize'); + let mut gas_before = get_available_gas(); - while let Option::Some(block) = blocks.pop_front() { + + for block in blocks { let height = chain_state.block_height + 1; match chain_state.validate_and_apply(block) { Result::Ok(new_chain_state) => { @@ -28,6 +44,7 @@ fn test( } } }; + if chain_state != expected_chain_state { println!( "FAIL: block={} error='expected state {:?}, actual {:?}'", diff --git a/src/types/block.cairo b/src/types/block.cairo index 4dd66dd0..be4765c4 100644 --- a/src/types/block.cairo +++ b/src/types/block.cairo @@ -8,7 +8,7 @@ use crate::utils::numeric::u32_byte_reverse; use super::transaction::Transaction; /// Represents a block in the blockchain. -#[derive(Drop, Copy, Debug, PartialEq, Default)] +#[derive(Drop, Copy, Debug, PartialEq, Default, Serde)] pub struct Block { /// Block header. pub header: Header, @@ -17,7 +17,7 @@ pub struct Block { } /// Represents block contents. -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub enum TransactionData { /// Merkle root of all transactions in the block. /// This variant is used for header-only validation mode (light client). @@ -37,7 +37,7 @@ pub enum TransactionData { /// In order to do the calculation we just need data about the block that is strictly necessary, /// but not the data we can calculate like merkle root or data that we already have /// like previous_block_hash (in the previous chain state). -#[derive(Drop, Copy, Debug, PartialEq, Default)] +#[derive(Drop, Copy, Debug, PartialEq, Default, Serde)] pub struct Header { /// The version of the block. pub version: u32, diff --git a/src/types/chain_state.cairo b/src/types/chain_state.cairo index 2335cdc1..25010611 100644 --- a/src/types/chain_state.cairo +++ b/src/types/chain_state.cairo @@ -13,7 +13,7 @@ use crate::validation::{ use super::block::{BlockHash, Block, TransactionData}; /// Represents the state of the blockchain. -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub struct ChainState { /// Height of the current block. pub block_height: u32, diff --git a/src/types/transaction.cairo b/src/types/transaction.cairo index b1a69039..5e68d3a2 100644 --- a/src/types/transaction.cairo +++ b/src/types/transaction.cairo @@ -8,7 +8,7 @@ use crate::codec::{Encode, TransactionCodec}; /// Represents a transaction. /// https://learnmeabitcoin.com/technical/transaction/ -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub struct Transaction { /// The version of the transaction. pub version: u32, @@ -29,7 +29,7 @@ pub struct Transaction { /// Input of a transaction. /// https://learnmeabitcoin.com/technical/transaction/input/ -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub struct TxIn { /// The signature script which satisfies the conditions placed in the txo pubkey script /// or coinbase script that contains block height (since 227,836) and miner nonce (optional). @@ -77,7 +77,7 @@ pub struct TxIn { /// one by one, first inputs then outputs. Output validation might put something to the /// cache while input validation might remove an item, thus it's important to maintain /// the order. -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub struct OutPoint { /// The hash of the referenced transaction. pub txid: Hash, @@ -112,7 +112,7 @@ pub struct OutPoint { /// - Do nothing in case of a provably unspendable output /// /// Read more: https://en.bitcoin.it/wiki/Script#Provably_Unspendable/Prunable_Outputs -#[derive(Drop, Copy, Debug, PartialEq)] +#[derive(Drop, Copy, Debug, PartialEq, Serde)] pub struct TxOut { /// The value of the output in satoshis. /// Can be in range [0, 21_000_000] BTC (including both ends). @@ -125,6 +125,19 @@ pub struct TxOut { pub cached: bool, } +impl ByteArraySnapSerde of Serde<@ByteArray> { + fn serialize(self: @@ByteArray, ref output: Array) { + (*self).serialize(ref output); + } + + fn deserialize(ref serialized: Span) -> Option<@ByteArray> { + match Serde::deserialize(ref serialized) { + Option::Some(res) => Option::Some(@res), + Option::None => Option::None, + } + } +} + impl TxOutDefault of Default { fn default() -> TxOut { TxOut { value: 0, pk_script: @"", cached: false, } diff --git a/src/utils/hash.cairo b/src/utils/hash.cairo index 9542870a..d2e9278f 100644 --- a/src/utils/hash.cairo +++ b/src/utils/hash.cairo @@ -7,7 +7,7 @@ use super::bit_shifts::{shl, shr}; /// 256-bit hash digest. /// Represented as an array of 4-byte words. -#[derive(Copy, Drop, Debug, Default)] +#[derive(Copy, Drop, Debug, Default, Serde)] pub struct Hash { pub value: [u32; 8] } diff --git a/src/utils/numeric.cairo b/src/utils/numeric.cairo index 51992bd0..1ad6c817 100644 --- a/src/utils/numeric.cairo +++ b/src/utils/numeric.cairo @@ -10,7 +10,6 @@ pub fn u32_byte_reverse(word: u32) -> u32 { return byte0 + byte1 + byte2 + byte3; } - #[cfg(test)] mod tests { use super::u32_byte_reverse; diff --git a/tests/data/full_169.json b/tests/data/full_169.json index 555bde63..0407f7a0 100644 --- a/tests/data/full_169.json +++ b/tests/data/full_169.json @@ -33,12 +33,10 @@ { "version": 1, "is_segwit": false, - "lock_time": 0, "inputs": [ { "script": "0x04ffff001d0102", "sequence": 4294967295, - "witness": [], "previous_output": { "txid": "0000000000000000000000000000000000000000000000000000000000000000", "vout": 4294967295, @@ -50,7 +48,8 @@ "block_height": 0, "block_time": 0, "is_coinbase": false - } + }, + "witness": [] } ], "outputs": [ @@ -59,17 +58,16 @@ "pk_script": "0x4104d46c4968bde02899d2aa0963367c7a6ce34eec332b32e42e5f3407e052d64ac625da6f0718e7b302140434bd725706957c092db53805b821a85b23a7ac61725bac", "cached": false } - ] + ], + "lock_time": 0 }, { "version": 1, "is_segwit": false, - "lock_time": 0, "inputs": [ { "script": "0x47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901", "sequence": 4294967295, - "witness": [], "previous_output": { "txid": "0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9", "vout": 0, @@ -81,7 +79,8 @@ "block_height": 9, "block_time": 1231473279, "is_coinbase": true - } + }, + "witness": [] } ], "outputs": [ @@ -95,7 +94,8 @@ "pk_script": "0x410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac", "cached": false } - ] + ], + "lock_time": 0 } ] }