use crate::activation::IsForkActive;
use crate::block::calculate_tx_id;
use crate::error::{ConsensusError, Result};
use crate::transaction::is_coinbase;
use crate::types::*;
use blvm_spec_lock::spec_locked;
#[cfg(feature = "production")]
pub type Bip30Index = rustc_hash::FxHashMap<crate::types::Hash, usize>;
#[cfg(not(feature = "production"))]
pub type Bip30Index = std::collections::HashMap<crate::types::Hash, usize>;
pub fn build_bip30_index(utxo_set: &UtxoSet) -> Bip30Index {
let mut index = Bip30Index::default();
for (outpoint, utxo) in utxo_set.iter() {
if utxo.is_coinbase {
*index.entry(outpoint.hash).or_insert(0) += 1;
}
}
index
}
#[spec_locked("5.4.1")]
pub fn check_bip30(
block: &Block,
utxo_set: &UtxoSet,
bip30_index: Option<&Bip30Index>,
height: Natural,
activation: &impl IsForkActive,
coinbase_txid: Option<&Hash>,
) -> Result<bool> {
if !activation.is_fork_active(ForkId::Bip30, height) {
return Ok(true);
}
let coinbase = block.transactions.first();
if let Some(tx) = coinbase {
if !is_coinbase(tx) {
return Ok(true);
}
let txid = coinbase_txid
.copied()
.unwrap_or_else(|| calculate_tx_id(tx));
if let Some(index) = bip30_index {
if index.get(&txid).is_some_and(|&c| c > 0) {
return Ok(false);
}
return Ok(true);
}
for (outpoint, _utxo) in utxo_set.iter() {
if outpoint.hash == txid {
return Ok(false);
}
}
}
Ok(true)
}
#[spec_locked("5.4.2")]
pub fn check_bip34(block: &Block, height: Natural, activation: &impl IsForkActive) -> Result<bool> {
if !activation.is_fork_active(ForkId::Bip34, height) {
return Ok(true);
}
let coinbase = block.transactions.first();
if let Some(tx) = coinbase {
if !is_coinbase(tx) {
return Ok(true);
}
let script_sig = &tx.inputs[0].script_sig;
if script_sig.is_empty() {
return Ok(false);
}
let extracted_height = extract_height_from_script_sig(script_sig)?;
if extracted_height != height {
return Ok(false);
}
}
Ok(true)
}
#[spec_locked("5.4")]
pub fn is_bip54_active_at(
height: Natural,
network: crate::types::Network,
activation_override: Option<u64>,
) -> bool {
let activation = match activation_override {
Some(h) => h,
None => match network {
crate::types::Network::Mainnet => crate::constants::BIP54_ACTIVATION_MAINNET,
crate::types::Network::Testnet => crate::constants::BIP54_ACTIVATION_TESTNET,
crate::types::Network::Regtest => crate::constants::BIP54_ACTIVATION_REGTEST,
},
};
height >= activation
}
#[spec_locked("5.4")]
pub fn is_bip54_active(height: Natural, network: crate::types::Network) -> bool {
is_bip54_active_at(height, network, None)
}
#[spec_locked("5.4")]
pub fn check_bip54_coinbase(coinbase: &Transaction, height: Natural) -> bool {
let required_lock_time = height.saturating_sub(13);
if coinbase.lock_time != required_lock_time {
return false;
}
if coinbase.inputs.is_empty() {
return false;
}
if coinbase.inputs[0].sequence == 0xffff_ffff {
return false;
}
true
}
fn extract_height_from_script_sig(script_sig: &[u8]) -> Result<Natural> {
if script_sig.is_empty() {
return Err(ConsensusError::BlockValidation(
"Empty coinbase scriptSig".into(),
));
}
let first_byte = script_sig[0];
if first_byte == 0x00 {
return Ok(0);
}
if (1..=0x4b).contains(&first_byte) {
let len = first_byte as usize;
if script_sig.len() < 1 + len {
return Err(ConsensusError::BlockValidation(
"Invalid scriptSig length".into(),
));
}
let height_bytes = &script_sig[1..1 + len];
let mut height = 0u64;
for (i, &byte) in height_bytes.iter().enumerate() {
if i >= 8 {
return Err(ConsensusError::BlockValidation(
"Height value too large".into(),
));
}
height |= (byte as u64) << (i * 8);
}
return Ok(height);
}
if first_byte == 0x4c {
if script_sig.len() < 2 {
return Err(ConsensusError::BlockValidation(
"Invalid OP_PUSHDATA1".into(),
));
}
let len = script_sig[1] as usize;
if script_sig.len() < 2 + len {
return Err(ConsensusError::BlockValidation(
"Invalid scriptSig length".into(),
));
}
let height_bytes = &script_sig[2..2 + len];
let mut height = 0u64;
for (i, &byte) in height_bytes.iter().enumerate() {
if i >= 8 {
return Err(ConsensusError::BlockValidation(
"Height value too large".into(),
));
}
height |= (byte as u64) << (i * 8);
}
return Ok(height);
}
if first_byte == 0x4d {
if script_sig.len() < 3 {
return Err(ConsensusError::BlockValidation(
"Invalid OP_PUSHDATA2".into(),
));
}
let len = u16::from_le_bytes([script_sig[1], script_sig[2]]) as usize;
if script_sig.len() < 3 + len {
return Err(ConsensusError::BlockValidation(
"Invalid scriptSig length".into(),
));
}
let height_bytes = &script_sig[3..3 + len];
let mut height = 0u64;
for (i, &byte) in height_bytes.iter().enumerate() {
if i >= 8 {
return Err(ConsensusError::BlockValidation(
"Height value too large".into(),
));
}
height |= (byte as u64) << (i * 8);
}
return Ok(height);
}
Err(ConsensusError::BlockValidation(
"Invalid height encoding in scriptSig".into(),
))
}
#[spec_locked("5.4.3")]
pub fn check_bip66(
signature: &[u8],
height: Natural,
activation: &impl IsForkActive,
) -> Result<bool> {
if !activation.is_fork_active(ForkId::Bip66, height) {
return Ok(true);
}
is_strict_der(signature)
}
fn is_strict_der(signature: &[u8]) -> Result<bool> {
if signature.len() < 9 {
return Ok(false);
}
if signature.len() > 73 {
return Ok(false);
}
if signature[0] != 0x30 {
return Ok(false);
}
if signature[1] != (signature.len() - 3) as u8 {
return Ok(false);
}
let len_r = signature[3] as usize;
if 5 + len_r >= signature.len() {
return Ok(false);
}
let len_s = signature[5 + len_r] as usize;
if (len_r + len_s + 7) != signature.len() {
return Ok(false);
}
if signature[2] != 0x02 {
return Ok(false);
}
if len_r == 0 {
return Ok(false);
}
if (signature[4] & 0x80) != 0 {
return Ok(false);
}
if len_r > 1 && signature[4] == 0x00 && (signature[5] & 0x80) == 0 {
return Ok(false);
}
if signature[len_r + 4] != 0x02 {
return Ok(false);
}
if len_s == 0 {
return Ok(false);
}
if (signature[len_r + 6] & 0x80) != 0 {
return Ok(false);
}
if len_s > 1 && signature[len_r + 6] == 0x00 && (signature[len_r + 7] & 0x80) == 0 {
return Ok(false);
}
Ok(true)
}
#[spec_locked("5.4.4")]
pub fn check_bip90(
block_version: i64,
height: Natural,
activation: &impl IsForkActive,
) -> Result<bool> {
if activation.is_fork_active(ForkId::Bip34, height) && block_version < 2 {
return Ok(false);
}
if activation.is_fork_active(ForkId::Bip66, height) && block_version < 3 {
return Ok(false);
}
if activation.is_fork_active(ForkId::Bip65, height) && block_version < 4 {
return Ok(false);
}
Ok(true)
}
pub fn check_bip30_network(
block: &Block,
utxo_set: &UtxoSet,
bip30_index: Option<&Bip30Index>,
height: Natural,
network: crate::types::Network,
coinbase_txid: Option<&Hash>,
) -> Result<bool> {
let table = crate::activation::ForkActivationTable::from_network(network);
check_bip30(block, utxo_set, bip30_index, height, &table, coinbase_txid)
}
pub fn check_bip34_network(
block: &Block,
height: Natural,
network: crate::types::Network,
) -> Result<bool> {
let table = crate::activation::ForkActivationTable::from_network(network);
check_bip34(block, height, &table)
}
pub fn check_bip66_network(
signature: &[u8],
height: Natural,
network: crate::types::Network,
) -> Result<bool> {
let table = crate::activation::ForkActivationTable::from_network(network);
check_bip66(signature, height, &table)
}
pub fn check_bip90_network(
block_version: i64,
height: Natural,
network: crate::types::Network,
) -> Result<bool> {
let table = crate::activation::ForkActivationTable::from_network(network);
check_bip90(block_version, height, &table)
}
pub fn check_bip147_network(
script_sig: &[u8],
script_pubkey: &[u8],
height: Natural,
network: Bip147Network,
) -> Result<bool> {
let table = match network {
Bip147Network::Mainnet => {
crate::activation::ForkActivationTable::from_network(crate::types::Network::Mainnet)
}
Bip147Network::Testnet => {
crate::activation::ForkActivationTable::from_network(crate::types::Network::Testnet)
}
Bip147Network::Regtest => {
crate::activation::ForkActivationTable::from_network(crate::types::Network::Regtest)
}
};
check_bip147(script_sig, script_pubkey, height, &table)
}
#[spec_locked("5.4.5")]
pub fn check_bip147(
script_sig: &[u8],
script_pubkey: &[u8],
height: Natural,
activation: &impl IsForkActive,
) -> Result<bool> {
if !activation.is_fork_active(ForkId::Bip147, height) {
return Ok(true);
}
if !script_pubkey.contains(&0xae) {
return Ok(true);
}
is_null_dummy(script_sig)
}
fn is_null_dummy(script_sig: &[u8]) -> Result<bool> {
if script_sig.is_empty() {
return Ok(false);
}
if script_sig.ends_with(&[0x00]) {
return Ok(true);
}
Ok(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bip147Network {
Mainnet,
Testnet,
Regtest,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::{BIP147_ACTIVATION_MAINNET, BIP66_ACTIVATION_MAINNET};
#[test]
fn test_bip30_basic() {
let transactions: Vec<Transaction> = vec![Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![0x04, 0x00, 0x00, 0x00, 0x00],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_0000_0000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
}];
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
};
let utxo_set = UtxoSet::default();
let result = check_bip30_network(
&block,
&utxo_set,
None,
0,
crate::types::Network::Mainnet,
None,
)
.unwrap();
assert!(result, "BIP30 should pass for new coinbase");
}
#[test]
fn test_bip34_before_activation() {
let transactions: Vec<Transaction> = vec![Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![0x04, 0x00, 0x00, 0x00, 0x00],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_0000_0000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
}];
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
};
let result = check_bip34_network(&block, 100_000, crate::types::Network::Mainnet).unwrap();
assert!(result, "BIP34 should pass before activation");
}
#[test]
fn test_bip34_after_activation() {
let height = crate::constants::BIP34_ACTIVATION_MAINNET;
let transactions: Vec<Transaction> = vec![Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![
0x03,
(height & 0xff) as u8,
((height >> 8) & 0xff) as u8,
((height >> 16) & 0xff) as u8,
],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_0000_0000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
}];
let block = Block {
header: BlockHeader {
version: 2, prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
};
let result = check_bip34_network(&block, height, crate::types::Network::Mainnet).unwrap();
assert!(result, "BIP34 should pass with correct height encoding");
}
#[test]
fn test_bip90_version_enforcement() {
let result = check_bip90_network(1, 100_000, crate::types::Network::Mainnet).unwrap();
assert!(result, "Version 1 should be valid before BIP34");
let result = check_bip90_network(
1,
crate::constants::BIP34_ACTIVATION_MAINNET,
crate::types::Network::Mainnet,
)
.unwrap();
assert!(
!result,
"Version 1 should be invalid after BIP34 activation"
);
let result = check_bip90_network(
2,
crate::constants::BIP34_ACTIVATION_MAINNET,
crate::types::Network::Mainnet,
)
.unwrap();
assert!(result, "Version 2 should be valid after BIP34 activation");
let result = check_bip90_network(2, 363_725, crate::types::Network::Mainnet).unwrap();
assert!(
!result,
"Version 2 should be invalid after BIP66 activation"
);
let result = check_bip90_network(3, 363_725, crate::types::Network::Mainnet).unwrap();
assert!(result, "Version 3 should be valid after BIP66 activation");
}
#[test]
fn test_bip30_duplicate_coinbase() {
use crate::block::calculate_tx_id;
let coinbase_tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![0x04, 0x00, 0x00, 0x00, 0x00],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_0000_0000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
};
let txid = calculate_tx_id(&coinbase_tx);
let mut utxo_set = UtxoSet::default();
utxo_set.insert(
OutPoint {
hash: txid,
index: 0,
},
std::sync::Arc::new(UTXO {
value: 50_0000_0000,
script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
}),
);
let transactions: Vec<Transaction> = vec![coinbase_tx];
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
};
let result = check_bip30_network(
&block,
&utxo_set,
None,
0,
crate::types::Network::Mainnet,
None,
)
.unwrap();
assert!(!result, "BIP30 should fail for duplicate coinbase");
}
#[test]
fn test_bip34_invalid_height() {
let height = crate::constants::BIP34_ACTIVATION_MAINNET;
let transactions: Vec<Transaction> = vec![Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0xffffffff,
},
script_sig: vec![0x03, 0x00, 0x00, 0x00], sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_0000_0000,
script_pubkey: vec![].into(),
}]
.into(),
lock_time: 0,
}];
let block = Block {
header: BlockHeader {
version: 2,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 1231006505,
bits: 0x1d00ffff,
nonce: 0,
},
transactions: transactions.into_boxed_slice(),
};
let result = check_bip34_network(&block, height, crate::types::Network::Mainnet).unwrap();
assert!(!result, "BIP34 should fail with incorrect height encoding");
}
#[test]
fn test_bip66_strict_der() {
let valid_der = vec![0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00];
let result = check_bip66_network(
&valid_der,
BIP66_ACTIVATION_MAINNET - 1,
crate::types::Network::Mainnet,
)
.unwrap();
assert!(
result || !result,
"BIP66 check should handle invalid DER gracefully"
);
let result =
check_bip66_network(&valid_der, 100_000, crate::types::Network::Mainnet).unwrap();
assert!(result, "BIP66 should pass before activation");
}
#[test]
fn test_bip147_null_dummy() {
let script_pubkey = vec![0x52, 0x21, 0x00, 0x21, 0x00, 0x52, 0xae];
let script_sig_valid = vec![0x00]; let result = check_bip147_network(
&script_sig_valid,
&script_pubkey,
BIP147_ACTIVATION_MAINNET,
Bip147Network::Mainnet,
)
.unwrap();
assert!(result, "BIP147 should pass with NULLDUMMY");
let script_sig_invalid = vec![0x01, 0x01]; let result = check_bip147_network(
&script_sig_invalid,
&script_pubkey,
BIP147_ACTIVATION_MAINNET,
Bip147Network::Mainnet,
)
.unwrap();
assert!(!result, "BIP147 should fail without NULLDUMMY");
let result = check_bip147_network(
&script_sig_invalid,
&script_pubkey,
100_000,
Bip147Network::Mainnet,
)
.unwrap();
assert!(result, "BIP147 should pass before activation");
}
}