Skip to content

Commit

Permalink
[feat] Check if transaction output is provably unspendable (#201)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Zaikin <[email protected]>
  • Loading branch information
Gerson2102 and m-kus authored Sep 24, 2024
1 parent 079e69e commit f485f76
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 6 deletions.
4 changes: 3 additions & 1 deletion packages/consensus/src/types/chain_state.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use core::fmt::{Display, Formatter, Error};
use crate::validation::{
difficulty::{validate_bits, adjust_difficulty}, coinbase::validate_coinbase,
timestamp::{validate_timestamp, next_prev_timestamps},
work::{validate_proof_of_work, compute_total_work}, block::{compute_and_validate_tx_data},
work::{validate_proof_of_work, compute_total_work},
block::{compute_and_validate_tx_data, validate_bip30_block_hash},
};
use super::block::{BlockHash, Block, TransactionData};
use super::utxo_set::UtxoSet;
Expand Down Expand Up @@ -90,6 +91,7 @@ pub impl BlockValidatorImpl of BlockValidator {

validate_proof_of_work(current_target, best_block_hash)?;
validate_bits(current_target, block.header.bits)?;
validate_bip30_block_hash(block_height, @best_block_hash)?;

Result::Ok(
ChainState {
Expand Down
134 changes: 133 additions & 1 deletion packages/consensus/src/validation/block.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
use crate::types::utxo_set::{UtxoSet, UtxoSetTrait};
use crate::types::transaction::{OutPoint, Transaction};
use crate::codec::{Encode, TransactionCodec};
use utils::{hash::Digest, merkle_tree::merkle_root, double_sha256::double_sha256_byte_array};
use utils::{
hash::{Digest, DigestTrait}, merkle_tree::merkle_root, double_sha256::double_sha256_byte_array,
};
use super::transaction::validate_transaction;
use core::num::traits::zero::Zero;

Expand Down Expand Up @@ -116,3 +118,133 @@ pub fn compute_and_validate_tx_data(

Result::Ok((total_fee, merkle_root(txids.span()), wtxid_root))
}

/// Validates that the block hash at certain heights matches the expected hash according to BIP-30.
///
/// This function ensures that for the two exceptional blocks affected by BIP-30 (heights 91722 and
/// 91812), the block hash matches the known hash for those heights. This prevents accepting
/// incorrect blocks at these critical heights, which could lead to security issues.
///
/// BIP-30 - Reject duplicate (https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki)
pub fn validate_bip30_block_hash(block_height: u32, block_hash: @Digest,) -> Result<(), ByteArray> {
if block_height == 91722 {
let expected_hash = DigestTrait::new(
[
0x8ed04d57,
0xf2f3cd6c,
0xa6e55569,
0xdc1654e1,
0xf219847f,
0x66e726dc,
0xa2710200,
0x00000000,
]
);
if *block_hash != expected_hash {
return Result::Err("Block hash mismatch for BIP-30 exception at height 91722");
}
Result::Ok(())
} else if block_height == 91812 {
let expected_hash = DigestTrait::new(
[
0x2f6f306f,
0xd683deb8,
0x5d9314ef,
0xfdcf36af,
0x66d9e3ac,
0xaceb79d4,
0xaed00a00,
0x00000000,
]
);
if *block_hash != expected_hash {
return Result::Err("Block hash mismatch for BIP-30 exception at height 91812");
}
Result::Ok(())
} else {
Result::Ok(())
}
}


#[cfg(test)]
mod tests {
use super::{validate_bip30_block_hash};
use utils::hash::{DigestTrait};

#[test]
fn test_bip30_block_hash() {
// Expected hash for block at height 91722
let expected_hash = DigestTrait::new(
[
0x8ed04d57,
0xf2f3cd6c,
0xa6e55569,
0xdc1654e1,
0xf219847f,
0x66e726dc,
0xa2710200,
0x00000000,
]
);
let result = validate_bip30_block_hash(91722, @expected_hash);
assert_eq!(result, Result::Ok(()));
}

#[test]
fn test_bip30_block_hash_wrong_hash() {
// Expected hash for block at height 91722
let expected_hash = DigestTrait::new(
[
0x8ed04d56,
0xf2f3cd6c,
0xa6e55569,
0xdc1654e1,
0xf219847f,
0x66e726dc,
0xa2710200,
0x00000000,
]
);
let result = validate_bip30_block_hash(91722, @expected_hash);
assert_eq!(result, Result::Err("Block hash mismatch for BIP-30 exception at height 91722"));
}

#[test]
fn test_bip30_block_hash_height_91812() {
// Expected hash for block at height 91812
let expected_hash = DigestTrait::new(
[
0x2f6f306f,
0xd683deb8,
0x5d9314ef,
0xfdcf36af,
0x66d9e3ac,
0xaceb79d4,
0xaed00a00,
0x00000000,
]
);
let result = validate_bip30_block_hash(91812, @expected_hash);
assert_eq!(result, Result::Ok(()));
}

#[test]
fn test_bip30_block_hash_wrong_hash_91812() {
// Expected hash for block at height 91812
let expected_hash = DigestTrait::new(
[
0x9ed04d56,
0xf2f3cd6c,
0xa6e55569,
0xdc1654e1,
0xf219847f,
0x66e726dc,
0xa2710200,
0x00000000,
]
);
let result = validate_bip30_block_hash(91812, @expected_hash);
assert_eq!(result, Result::Err("Block hash mismatch for BIP-30 exception at height 91812"));
}
}
30 changes: 29 additions & 1 deletion packages/consensus/src/validation/coinbase.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,23 @@ fn validate_coinbase_outputs(
Result::Ok(())
}


/// Determines if the transaction outputs of a block at a given height are unspendable due to
/// BIP-30.
///
/// This function checks if the block height corresponds to one of the two exceptional blocks
/// where transactions with duplicate TXIDs were allowed.
pub fn is_bip30_unspendable(block_height: u32) -> bool {
block_height == 91722 || block_height == 91812
}

#[cfg(test)]
mod tests {
use crate::types::transaction::{TxIn, TxOut, Transaction, OutPoint};
use super::{
compute_block_reward, validate_coinbase, validate_coinbase_input,
validate_coinbase_sig_script, validate_coinbase_witness, validate_coinbase_outputs,
calculate_wtxid_commitment
calculate_wtxid_commitment, is_bip30_unspendable
};
use utils::{hex::{from_hex, hex_to_hash_rev}, hash::Digest};

Expand Down Expand Up @@ -662,5 +672,23 @@ mod tests {

validate_coinbase(@tx, total_fees, block_height, wtxid_root_hash).unwrap();
}

#[test]
fn test_is_bip30_unspendable() {
let block_height = 91722;
let result = is_bip30_unspendable(block_height);

assert_eq!(result, true);

let block_height = 91812;
let result = is_bip30_unspendable(block_height);

assert_eq!(result, true);

let block_height = 9;
let result = is_bip30_unspendable(block_height);

assert_eq!(result, false);
}
}

37 changes: 34 additions & 3 deletions packages/consensus/src/validation/transaction.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use crate::validation::locktime::{
};
use utils::hash::Digest;

const OP_RETURN: u8 = 0x6a;
const MAX_SCRIPT_SIZE: u32 = 10000;

/// Validate transaction and return transaction fee.
///
/// This does not include script checks and outpoint inclusion verification.
Expand Down Expand Up @@ -122,6 +125,15 @@ fn validate_coinbase_maturity(output_height: u32, block_height: u32) -> Result<(
}
}

/// Checks if a public key script (pubscript) is provably unspendable.
///
/// A pubscript is considered unspendable if:
/// - It starts with `OP_RETURN`.
/// - Its size exceeds the maximum allowed script size.
fn is_pubscript_unspendable(pubscript: @ByteArray) -> bool {
pubscript[0].into() == OP_RETURN || pubscript.len() > MAX_SCRIPT_SIZE
}

#[cfg(test)]
mod tests {
use core::dict::Felt252Dict;
Expand All @@ -131,9 +143,7 @@ mod tests {
use utils::{
hash::Digest, hex::{from_hex, hex_to_hash_rev}, double_sha256::double_sha256_byte_array
};
use super::validate_transaction;

// TODO: tests for coinbase maturity
use super::{validate_transaction, is_pubscript_unspendable, MAX_SCRIPT_SIZE};

#[test]
fn test_tx_fee() {
Expand Down Expand Up @@ -891,4 +901,25 @@ mod tests {
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "The output has already been spent");
}

#[test]
fn test_pubscript_starts_with_op_return() {
let op_return_script = from_hex("6a146f6e65207069656365206f6620646174612068657265");
assert!(is_pubscript_unspendable(@op_return_script));
}

#[test]
fn test_pubscript_within_size_limit() {
let normal_script = from_hex("76a91489abcdefabbaabbaabbaabbaabbaabbaabbaabba88ac");
assert!(!is_pubscript_unspendable(@normal_script));
}

#[test]
fn test_pubscript_exceeds_max_size() {
let mut large_script: ByteArray = Default::default();
for _ in 0..(MAX_SCRIPT_SIZE + 1) {
large_script.append_byte(0x00);
};
assert!(is_pubscript_unspendable(@large_script));
}
}

0 comments on commit f485f76

Please sign in to comment.