Skip to content

Commit

Permalink
Use word array for serializing transactions (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-kus authored Nov 25, 2024
1 parent 7cf04a7 commit 4a68be6
Show file tree
Hide file tree
Showing 10 changed files with 506 additions and 190 deletions.
141 changes: 77 additions & 64 deletions packages/consensus/src/codec.cairo

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions packages/consensus/src/types/block.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use core::fmt::{Display, Formatter, Error};
use super::transaction::Transaction;
use utils::hash::Digest;
use utils::double_sha256::double_sha256_u32_array;
use utils::numeric::u32_byte_reverse;
use utils::double_sha256::double_sha256_word_array;
use utils::word_array::{WordArray, WordArrayTrait};

/// Represents a block in the blockchain.
#[derive(Drop, Copy, Debug, PartialEq, Default, Serde)]
Expand Down Expand Up @@ -56,17 +56,17 @@ pub struct Header {
pub impl BlockHashImpl of BlockHash {
/// Computes the hash of the block header given the missing fields.
fn hash(self: @Header, prev_block_hash: Digest, merkle_root: Digest) -> Digest {
let mut header_data_u32: Array<u32> = array![];
let mut words: WordArray = Default::default();

header_data_u32.append(u32_byte_reverse(*self.version));
header_data_u32.append_span(prev_block_hash.value.span());
header_data_u32.append_span(merkle_root.value.span());
words.append_u32_le(*self.version);
words.append_span(prev_block_hash.value.span());
words.append_span(merkle_root.value.span());

header_data_u32.append(u32_byte_reverse(*self.time));
header_data_u32.append(u32_byte_reverse(*self.bits));
header_data_u32.append(u32_byte_reverse(*self.nonce));
words.append_u32_le(*self.time);
words.append_u32_le(*self.bits);
words.append_u32_le(*self.nonce);

double_sha256_u32_array(header_data_u32)
double_sha256_word_array(words)
}
}

Expand Down
23 changes: 14 additions & 9 deletions packages/consensus/src/validation/block.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::types::utxo_set::{UtxoSet, UtxoSetTrait};
use crate::types::transaction::{OutPoint, Transaction};
use crate::codec::{Encode, TransactionCodec};
use crate::validation::{coinbase::is_coinbase_txid_duplicated, transaction::validate_transaction};
use utils::{hash::Digest, merkle_tree::merkle_root, double_sha256::double_sha256_byte_array,};
use utils::{hash::Digest, merkle_tree::merkle_root, double_sha256::double_sha256_word_array,};
use utils::word_array::WordArrayTrait;

const MAX_BLOCK_WEIGHT_LEGACY: usize = 1_000_000;
const MAX_BLOCK_WEIGHT: usize = 4_000_000;
Expand Down Expand Up @@ -45,29 +46,33 @@ pub fn compute_and_validate_tx_data(
let mut is_coinbase = true;

for tx in txs {
let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let tx_words_span = tx_words.span();
let tx_byte_len = tx_words.byte_len();

let txid = double_sha256_word_array(tx_words);

if block_height >= SEGWIT_BLOCK {
let tx_bytes_segwit = @tx
.encode_with_witness(tx_bytes_legacy); // SegWit transaction encoding
let tx_words_segwit = tx
.encode_with_witness(tx_words_span); // SegWit transaction encoding
let tx_byte_len_segwit = tx_words_segwit.byte_len();

/// The wTXID for the coinbase transaction must be set to all zeros. This is because
/// it's eventually going to contain the commitment inside it.
/// See https://learnmeabitcoin.com/technical/transaction/wtxid/#commitment
let wtxid = if is_coinbase {
Zero::zero()
} else {
double_sha256_byte_array(tx_bytes_segwit)
double_sha256_word_array(tx_words_segwit)
};

total_weight += 3 * tx_bytes_legacy.len()
+ tx_bytes_segwit.len(); // Calculate block weight with SegWit
total_weight += 3 * tx_byte_len
+ tx_byte_len_segwit; // Calculate block weight with SegWit

wtxids.append(wtxid); // Append wtxid to array
} else {
// For blocks before SegWit, only legacy tx weight is considered
total_weight += 4 * tx_bytes_legacy.len(); // Calculate block weight without SegWit
total_weight += 4 * tx_byte_len; // Calculate block weight without SegWit
}

txids.append(txid);
Expand Down
18 changes: 8 additions & 10 deletions packages/consensus/src/validation/coinbase.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! https://learnmeabitcoin.com/technical/mining/coinbase-transaction/

use crate::types::transaction::{Transaction, TxIn, TxOut};
use utils::{hash::{Digest, DigestIntoByteArray}, double_sha256::{double_sha256_byte_array}};
use utils::{hash::{Digest, DigestIntoByteArray}, double_sha256::double_sha256_word_array};
use utils::word_array::{WordArray, WordArrayTrait};

const BIP_34_BLOCK_HEIGHT: u32 = 227_836;
const BIP_141_BLOCK_HEIGHT: u32 = 481_824;
Expand Down Expand Up @@ -123,18 +124,15 @@ fn compute_block_reward(block_height: u32) -> u64 {

/// Calculates wtxid commitment.
fn calculate_wtxid_commitment(wtxid_root: Digest) -> Digest {
// Construct witness reserved value
// 0000000000000000000000000000000000000000000000000000000000000000
let witness_value_bytes: ByteArray =
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
let mut buffer: WordArray = Default::default();

// Convert wtxid_root to ByteArray
let wtxid_root_bytes: ByteArray = wtxid_root.into();
buffer.append_span(wtxid_root.value.span());

// Concat (witness root hash | witness reserved value)
let res = ByteArrayTrait::concat(@wtxid_root_bytes, @witness_value_bytes);
// Witness reserved value
// 0000000000000000000000000000000000000000000000000000000000000000
buffer.append_span(array![0, 0, 0, 0, 0, 0, 0, 0].span());

double_sha256_byte_array(@res)
double_sha256_word_array(buffer)
}

/// validates segwit output (BIP-141).
Expand Down
62 changes: 31 additions & 31 deletions packages/consensus/src/validation/transaction.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ mod tests {
use crate::types::transaction::{Transaction, TxIn, TxOut, OutPoint, OutPointHashTrait};
use crate::types::utxo_set::{UtxoSet, TX_OUTPUT_STATUS_UNSPENT};
use super::{validate_transaction, is_pubscript_unspendable, MAX_SCRIPT_SIZE};
use utils::{hex::{from_hex, hex_to_hash_rev}, double_sha256::double_sha256_byte_array};
use utils::{hex::{from_hex, hex_to_hash_rev}, double_sha256::double_sha256_word_array};

#[test]
fn test_tx_fee() {
Expand Down Expand Up @@ -182,8 +182,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

assert!(validate_transaction(@tx, 0, 0, 0, txid, ref utxo_set).is_err());
Expand Down Expand Up @@ -211,8 +211,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

let result = validate_transaction(@tx, 0, 0, 0, txid, ref utxo_set);
Expand Down Expand Up @@ -246,8 +246,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

let result = validate_transaction(@tx, 0, 0, 0, txid, ref utxo_set);
Expand Down Expand Up @@ -288,8 +288,8 @@ mod tests {
lock_time: 500000 // Block height locktime
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

// Transaction should be invalid when current block height is less than locktime
Expand Down Expand Up @@ -341,8 +341,8 @@ mod tests {
lock_time: 1600000000 // UNIX timestamp locktime
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

// Transaction should be invalid when current block time is not greater than locktime
Expand Down Expand Up @@ -393,8 +393,8 @@ mod tests {
lock_time: 1600000000 // UNIX timestamp locktime
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

// Transaction should still valid when current block time is not greater than locktime
Expand Down Expand Up @@ -442,8 +442,8 @@ mod tests {
lock_time: 500000 // Block height locktime
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

// Transaction should still valid when current block time is not greater than locktime
Expand Down Expand Up @@ -493,8 +493,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

validate_transaction(@tx, block_height, 0, 0, txid, ref utxo_set).unwrap_err();
Expand Down Expand Up @@ -536,8 +536,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

validate_transaction(@tx, block_height, 0, 0, txid, ref utxo_set).unwrap();
Expand Down Expand Up @@ -580,8 +580,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

validate_transaction(@tx, block_height, 0, 0, txid, ref utxo_set).unwrap();
Expand Down Expand Up @@ -624,8 +624,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);

let mut cache: Felt252Dict<u8> = Default::default();
let outpoint_hash = (*tx.inputs[0]).previous_output.hash();
Expand Down Expand Up @@ -671,8 +671,8 @@ mod tests {
lock_time: 0
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);

let mut cache: Felt252Dict<u8> = Default::default();
let outpoint_hash = (*tx.inputs[0]).previous_output.hash();
Expand Down Expand Up @@ -718,8 +718,8 @@ mod tests {
lock_time: 0,
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);

let mut cache: Felt252Dict<u8> = Default::default();
let outpoint = OutPoint {
Expand Down Expand Up @@ -789,8 +789,8 @@ mod tests {
lock_time: 0,
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);

let mut cache: Felt252Dict<u8> = Default::default();
let outpoint_hash = (*tx.inputs[0]).previous_output.hash();
Expand Down Expand Up @@ -852,8 +852,8 @@ mod tests {
lock_time: 0,
};

let tx_bytes_legacy = @tx.encode();
let txid = double_sha256_byte_array(tx_bytes_legacy);
let tx_words = tx.encode();
let txid = double_sha256_word_array(tx_words);
let mut utxo_set: UtxoSet = Default::default();

let result = validate_transaction(@tx, block_height, 0, 0, txid, ref utxo_set);
Expand Down
41 changes: 17 additions & 24 deletions packages/utils/src/double_sha256.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use super::word_array::WordArrayTrait;
//! Helpers for calculating double SHA256 hash digest.

use super::hash::{Digest, DigestTrait};
use super::sha256::{compute_sha256_byte_array, compute_sha256_u32_array};
use super::sha256::compute_sha256_u32_array;
use super::word_array::WordArray;

/// Calculates double sha256 digest of a concatenation of two hashes.
pub fn double_sha256_parent(a: @Digest, b: @Digest) -> Digest {
Expand All @@ -15,46 +17,37 @@ pub fn double_sha256_parent(a: @Digest, b: @Digest) -> Digest {
DigestTrait::new(compute_sha256_u32_array(input2, 0, 0))
}

/// Calculates double sha256 digest of bytes.
pub fn double_sha256_byte_array(bytes: @ByteArray) -> Digest {
let mut input2: Array<u32> = array![];
input2.append_span(compute_sha256_byte_array(bytes).span());

DigestTrait::new(compute_sha256_u32_array(input2, 0, 0))
}
pub fn double_sha256_word_array(words: WordArray) -> Digest {
let (input, last_input_word, last_input_num_bytes) = words.into_components();

/// Calculates double sha256 digest of an array of full 4 byte words.
///
/// It's important that there are no trailing bytes, otherwise the
/// data will be truncated.
pub fn double_sha256_u32_array(words: Array<u32>) -> Digest {
let mut input2: Array<u32> = array![];
input2.append_span(compute_sha256_u32_array(words, 0, 0).span());
input2
.append_span(compute_sha256_u32_array(input, last_input_word, last_input_num_bytes).span());

DigestTrait::new(compute_sha256_u32_array(input2, 0, 0))
}

#[cfg(test)]
mod tests {
use crate::{hex::from_hex, hash::Digest};
use super::{double_sha256_byte_array, double_sha256_u32_array, double_sha256_parent};
use crate::{hex::from_hex, hash::Digest, word_array::hex::words_from_hex};
use super::{double_sha256_word_array, double_sha256_parent};

#[test]
fn test_double_sha256_byte_array() {
fn test_double_sha256_word_array() {
// hashlib.sha256(sha256(b"bitcoin").digest()).hexdigest()
assert_eq!(
double_sha256_byte_array(@"bitcoin").into(),
double_sha256_word_array(words_from_hex("626974636f696e")).into(),
from_hex("f1ef1bf105d788352c052453b15a913403be59b90ddf9f7c1f937edee8938dc5")
)
}
);

#[test]
fn test_double_sha256_u32_array() {
// hashlib.sha256(sha256(bytes.fromhex("00000001000000020000000300000004000000050000000600000007")).digest()).hexdigest()
assert_eq!(
double_sha256_u32_array(array![1, 2, 3, 4, 5, 6, 7]).into(),
double_sha256_word_array(
words_from_hex("00000001000000020000000300000004000000050000000600000007")
)
.into(),
from_hex("489b8eeb4024cb77ab057616ebf7f8d4405aa0bd3ad5f42e6b4c20580e011ac4")
)
);
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/src/hex.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub fn hex_to_hash_rev(hex_string: ByteArray) -> Digest {
}
}

fn hex_char_to_nibble(hex_char: u8) -> u8 {
pub fn hex_char_to_nibble(hex_char: u8) -> u8 {
if hex_char >= 48 && hex_char <= 57 {
// 0-9
hex_char - 48
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod hash;
pub mod merkle_tree;
pub mod numeric;
pub mod sort;
pub mod word_array;


// pub mod sha256;
Expand Down
Loading

0 comments on commit 4a68be6

Please sign in to comment.