use crate::network::transport::TransportType;
use anyhow::Result;
use blvm_protocol::segwit::Witness;
use blvm_protocol::{Block, BlockHeader, Hash, Transaction};
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
use std::hash::Hasher;
use std::time::{Duration, Instant};
pub use blvm_protocol::bip152::{CompactBlock, ShortTxId};
pub const MAX_COMPACT_BLOCK_TXS: usize = 100_000;
pub const MAX_COMPACT_MISSING: usize = 128;
pub const MAX_PENDING_COMPACT: usize = 4;
pub const MAX_PENDING_COMPACT_BYTES: usize = 8 * 1024 * 1024;
pub const COMPACT_ASSEMBLY_DEADLINE: Duration = Duration::from_secs(60);
pub fn calculate_tx_hash(tx: &Transaction) -> Hash {
let mut data = Vec::new();
data.extend_from_slice(&(tx.version as u32).to_le_bytes());
data.extend_from_slice(&encode_varint(tx.inputs.len() as u64));
for input in &tx.inputs {
data.extend_from_slice(&input.prevout.hash);
data.extend_from_slice(&input.prevout.index.to_le_bytes());
data.extend_from_slice(&encode_varint(input.script_sig.len() as u64));
data.extend_from_slice(&input.script_sig);
data.extend_from_slice(&(input.sequence as u32).to_le_bytes());
}
data.extend_from_slice(&encode_varint(tx.outputs.len() as u64));
for output in &tx.outputs {
data.extend_from_slice(&(output.value as u64).to_le_bytes());
data.extend_from_slice(&encode_varint(output.script_pubkey.len() as u64));
data.extend_from_slice(&output.script_pubkey);
}
data.extend_from_slice(&(tx.lock_time as u32).to_le_bytes());
let hash1 = Sha256::digest(&data);
let hash2 = Sha256::digest(hash1);
let mut result = [0u8; 32];
result.copy_from_slice(&hash2);
result
}
fn encode_varint(value: u64) -> Vec<u8> {
if value < 0xfd {
vec![value as u8]
} else if value <= 0xffff {
let mut result = vec![0xfd];
result.extend_from_slice(&(value as u16).to_le_bytes());
result
} else if value <= 0xffffffff {
let mut result = vec![0xfe];
result.extend_from_slice(&(value as u32).to_le_bytes());
result
} else {
let mut result = vec![0xff];
result.extend_from_slice(&value.to_le_bytes());
result
}
}
fn short_id_keys(header: &BlockHeader, nonce: u64) -> (u64, u64) {
let mut data = blvm_protocol::serialization::serialize_block_header(header);
data.extend_from_slice(&nonce.to_le_bytes());
let hash = Sha256::digest(&data);
let k0 = u64::from_le_bytes(hash[0..8].try_into().expect("sha256 prefix"));
let k1 = u64::from_le_bytes(hash[8..16].try_into().expect("sha256 prefix"));
(k0, k1)
}
pub fn calculate_short_tx_id(header: &BlockHeader, wtxid: &Hash, nonce: u64) -> ShortTxId {
let (k0, k1) = short_id_keys(header, nonce);
use siphasher::sip::SipHasher24;
let mut hasher = SipHasher24::new_with_keys(k0, k1);
hasher.write(wtxid);
let hash_result = hasher.finish();
let mut short_id = [0u8; 6];
short_id.copy_from_slice(&hash_result.to_le_bytes()[..6]);
short_id
}
pub fn reconstruct_block(
compact_block: &CompactBlock,
mempool_txs: &HashMap<Hash, Transaction>,
) -> Result<Vec<usize>> {
let mut missing_indices = Vec::new();
let mut reconstructed_txs = Vec::new();
let prefilled: HashSet<usize> = compact_block
.prefilled_txs
.iter()
.map(|(index, _, _)| *index)
.collect();
let mut next_block_index = 0usize;
let mut by_short: HashMap<ShortTxId, Vec<&Transaction>> = HashMap::new();
for (tx_hash, tx) in mempool_txs {
let short_id = calculate_short_tx_id(&compact_block.header, tx_hash, compact_block.nonce);
by_short.entry(short_id).or_default().push(tx);
}
for &short_id in &compact_block.short_ids {
while prefilled.contains(&next_block_index) {
next_block_index = next_block_index.saturating_add(1);
}
let block_index = next_block_index;
next_block_index = next_block_index.saturating_add(1);
let matches = by_short.get(&short_id).map(Vec::as_slice).unwrap_or(&[]);
if let [tx] = matches {
let tx = *tx;
let conflicts_match = reconstructed_txs
.iter()
.any(|(_, existing_tx)| has_conflict_with_tx(tx, existing_tx));
let conflicts_prefilled = compact_block
.prefilled_txs
.iter()
.any(|(_, existing_tx, _)| has_conflict_with_tx(tx, existing_tx));
if conflicts_match || conflicts_prefilled {
missing_indices.push(block_index);
} else {
reconstructed_txs.push((block_index, (*tx).clone()));
}
} else {
missing_indices.push(block_index);
}
}
Ok(missing_indices)
}
#[derive(Debug, Clone)]
pub struct CompactAssembly {
pub header: BlockHeader,
pub nonce: u64,
pub slots: Vec<Option<(Transaction, Option<Vec<Witness>>)>>,
pub missing: Vec<usize>,
pub started: Instant,
pub short_id_bytes: usize,
}
impl CompactAssembly {
pub fn expired_at(&self, now: Instant) -> bool {
now.saturating_duration_since(self.started) >= COMPACT_ASSEMBLY_DEADLINE
}
}
pub fn assembly_retained_bytes(assembly: &CompactAssembly) -> usize {
let tx_bytes = assembly
.slots
.iter()
.flatten()
.map(|(tx, witness)| serialized_slot_bytes(tx, witness.as_deref()))
.sum::<usize>();
let slot_bytes = assembly
.slots
.capacity()
.saturating_mul(std::mem::size_of::<
Option<(Transaction, Option<Vec<Witness>>)>,
>());
tx_bytes
.saturating_add(slot_bytes)
.saturating_add(assembly.short_id_bytes)
}
fn serialized_slot_bytes(tx: &Transaction, witness: Option<&[Witness]>) -> usize {
let body = blvm_protocol::serialization::transaction::serialize_transaction(tx).len();
let witness_bytes = witness
.map(|stacks| {
stacks
.iter()
.map(|stack| stack.iter().map(|item| item.len()).sum::<usize>())
.sum::<usize>()
})
.unwrap_or(0);
body.saturating_add(witness_bytes)
}
pub struct CompactBudget {
pub assemblies: usize,
pub retained_bytes: usize,
pub same_hash: bool,
}
pub enum CompactDecision {
Hold(CompactAssembly),
FullBlock,
KeepCurrent,
}
pub fn decide_compact_block(
compact: &CompactBlock,
matches: &HashMap<Hash, (Transaction, Option<Vec<Witness>>)>,
budget: CompactBudget,
) -> Result<CompactDecision> {
let total = compact
.short_ids
.len()
.saturating_add(compact.prefilled_txs.len());
if total > MAX_COMPACT_BLOCK_TXS {
return Ok(CompactDecision::FullBlock);
}
let assembly = begin_compact_assembly(compact, matches)?;
if assembly.missing.len() > MAX_COMPACT_MISSING
|| assembly
.missing
.iter()
.any(|index| u16::try_from(*index).is_err())
{
return Ok(CompactDecision::FullBlock);
}
let adding = if budget.same_hash { 0 } else { 1 };
if budget.assemblies.saturating_add(adding) > MAX_PENDING_COMPACT {
return Ok(CompactDecision::FullBlock);
}
let next_bytes = budget
.retained_bytes
.saturating_add(assembly_retained_bytes(&assembly));
if next_bytes > MAX_PENDING_COMPACT_BYTES {
if budget.same_hash {
return Ok(CompactDecision::KeepCurrent);
}
return Ok(CompactDecision::FullBlock);
}
Ok(CompactDecision::Hold(assembly))
}
pub fn begin_compact_assembly(
compact_block: &CompactBlock,
mempool_txs: &HashMap<Hash, (Transaction, Option<Vec<blvm_protocol::segwit::Witness>>)>,
) -> Result<CompactAssembly> {
let tx_only: HashMap<Hash, Transaction> = mempool_txs
.iter()
.map(|(hash, (tx, _))| (*hash, tx.clone()))
.collect();
let missing = reconstruct_block(compact_block, &tx_only)?;
let n = compact_block.short_ids.len() + compact_block.prefilled_txs.len();
let mut slots = vec![None; n];
for (index, tx, witness) in &compact_block.prefilled_txs {
if *index >= n {
anyhow::bail!("prefilled compact-block index {index} is past {n}");
}
slots[*index] = Some((tx.clone(), witness.clone()));
}
let prefilled: HashSet<usize> = compact_block
.prefilled_txs
.iter()
.map(|(index, _, _)| *index)
.collect();
let mut by_short: HashMap<ShortTxId, &(Transaction, Option<Vec<Witness>>)> = HashMap::new();
for (tx_hash, pair) in mempool_txs {
let short_id = calculate_short_tx_id(&compact_block.header, tx_hash, compact_block.nonce);
by_short.entry(short_id).or_insert(pair);
}
let missing_set: HashSet<usize> = missing.iter().copied().collect();
let mut next_block_index = 0usize;
for &short_id in &compact_block.short_ids {
while prefilled.contains(&next_block_index) {
next_block_index = next_block_index.saturating_add(1);
}
let block_index = next_block_index;
next_block_index = next_block_index.saturating_add(1);
if missing_set.contains(&block_index) {
continue;
}
let found = by_short.get(&short_id).map(|pair| (*pair).clone());
if let Some(filled) = found {
if block_index < slots.len() {
slots[block_index] = Some(filled);
}
}
}
Ok(CompactAssembly {
header: compact_block.header.clone(),
nonce: compact_block.nonce,
slots,
missing,
started: Instant::now(),
short_id_bytes: 6usize.saturating_mul(compact_block.short_ids.len()),
})
}
pub fn apply_blocktxn(
assembly: &mut CompactAssembly,
transactions: &[Transaction],
witnesses: Option<&[Vec<blvm_protocol::segwit::Witness>]>,
) -> Result<()> {
if transactions.len() != assembly.missing.len() {
anyhow::bail!(
"blocktxn count {} does not match {} missing positions",
transactions.len(),
assembly.missing.len()
);
}
for (i, &index) in assembly.missing.iter().enumerate() {
if index >= assembly.slots.len() {
anyhow::bail!("blocktxn index {index} is past the compact block");
}
let witness = witnesses.and_then(|all| all.get(i).cloned());
assembly.slots[index] = Some((transactions[i].clone(), witness));
}
assembly.missing.clear();
Ok(())
}
pub fn completed_compact_block(
assembly: &CompactAssembly,
) -> Result<(Block, Vec<Vec<blvm_protocol::segwit::Witness>>)> {
let mut transactions = Vec::with_capacity(assembly.slots.len());
let mut witnesses = Vec::with_capacity(assembly.slots.len());
for (index, slot) in assembly.slots.iter().enumerate() {
let Some((tx, witness)) = slot else {
anyhow::bail!("compact block position {index} is still missing");
};
let stacks = witness
.clone()
.unwrap_or_else(|| tx.inputs.iter().map(|_| Vec::new()).collect());
witnesses.push(stacks);
transactions.push(tx.clone());
}
Ok((
Block {
header: assembly.header.clone(),
transactions: transactions.into_boxed_slice(),
},
witnesses,
))
}
fn has_conflict_with_tx(tx1: &Transaction, tx2: &Transaction) -> bool {
for input1 in &tx1.inputs {
for input2 in &tx2.inputs {
if input1.prevout == input2.prevout {
return true;
}
}
}
false
}
pub fn create_compact_block(
block: &Block,
nonce: u64,
prefilled_indices: &HashSet<usize>,
) -> CompactBlock {
create_compact_block_with_witnesses(block, nonce, prefilled_indices, &[])
}
pub fn create_compact_block_with_witnesses(
block: &Block,
nonce: u64,
prefilled_indices: &HashSet<usize>,
witnesses: &[Option<Vec<blvm_protocol::segwit::Witness>>],
) -> CompactBlock {
let mut short_ids = Vec::new();
let mut prefilled_txs = Vec::new();
for (index, tx) in block.transactions.iter().enumerate() {
if prefilled_indices.contains(&index) {
let witness = witnesses
.get(index)
.and_then(|stacks| stacks.clone())
.filter(|stacks| stacks.iter().any(|stack| !stack.is_empty()));
prefilled_txs.push((index, tx.clone(), witness));
} else {
let witness = witnesses.get(index).and_then(|stacks| stacks.as_deref());
let wtxid = crate::network::txhash::calculate_wtxid(tx, witness);
let short_id = calculate_short_tx_id(&block.header, &wtxid, nonce);
short_ids.push(short_id);
}
}
CompactBlock {
header: block.header.clone(),
nonce,
short_ids,
prefilled_txs,
}
}
pub fn should_prefer_compact_blocks(transport_type: TransportType) -> bool {
match transport_type {
#[cfg(feature = "quinn")]
TransportType::Quinn => true, #[cfg(feature = "iroh")]
TransportType::Iroh => true, #[cfg(any(feature = "quinn", feature = "iroh"))]
TransportType::Tcp => false, #[cfg(not(any(feature = "quinn", feature = "iroh")))]
TransportType::Tcp => false, }
}
pub fn negotiate_optimizations(
transport_type: TransportType,
peer_services: u64,
) -> (u64, bool, bool) {
use blvm_protocol::bip157::NODE_COMPACT_FILTERS;
let compact_version = recommended_compact_block_version(transport_type);
let prefer_compact = should_prefer_compact_blocks(transport_type);
let supports_filters = (peer_services & NODE_COMPACT_FILTERS) != 0;
(compact_version, prefer_compact, supports_filters)
}
pub fn create_optimized_sendcmpct(
transport_type: TransportType,
peer_services: u64,
) -> crate::network::protocol::SendCmpctMessage {
use crate::network::protocol::SendCmpctMessage;
let (version, prefer_cmpct, _supports_filters) =
negotiate_optimizations(transport_type, peer_services);
SendCmpctMessage {
version,
prefer_cmpct: if prefer_cmpct { 1 } else { 0 },
}
}
pub fn recommended_compact_block_version(transport_type: TransportType) -> u64 {
match transport_type {
TransportType::Tcp => 1, #[cfg(feature = "quinn")]
TransportType::Quinn => 2, #[cfg(feature = "iroh")]
TransportType::Iroh => 2, }
}
pub fn is_quic_transport(transport_type: TransportType) -> bool {
match transport_type {
#[cfg(feature = "quinn")]
TransportType::Quinn => true,
#[cfg(feature = "iroh")]
TransportType::Iroh => true,
#[cfg(any(feature = "quinn", feature = "iroh"))]
TransportType::Tcp => false,
#[cfg(not(any(feature = "quinn", feature = "iroh")))]
TransportType::Tcp => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::transport::TransportType;
#[test]
fn test_calculate_short_tx_id() {
let tx_hash = [0u8; 32];
let nonce = 12345u64;
let header = BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let short_id = calculate_short_tx_id(&header, &tx_hash, nonce);
assert_eq!(short_id.len(), 6);
}
#[test]
fn short_id_uses_header_keys_and_witness_hash() {
use blvm_protocol::opcodes::OP_1;
let wtxid = [1u8; 32];
let nonce = 7u64;
let header = BlockHeader {
version: 1,
prev_block_hash: [2u8; 32],
merkle_root: [3u8; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let other_header = BlockHeader {
merkle_root: [4u8; 32],
..header.clone()
};
assert_ne!(
calculate_short_tx_id(&header, &wtxid, nonce),
calculate_short_tx_id(&other_header, &wtxid, nonce)
);
let tx = Transaction {
version: 1,
inputs: vec![blvm_protocol::TransactionInput {
prevout: blvm_protocol::OutPoint {
hash: [5u8; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: blvm_protocol::constants::SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![blvm_protocol::TransactionOutput {
value: 50_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let block = Block {
header: header.clone(),
transactions: vec![tx.clone()].into_boxed_slice(),
};
let bare = create_compact_block_with_witnesses(&block, nonce, &HashSet::new(), &[]);
let stacks = vec![vec![vec![OP_1]]];
let with_witness = create_compact_block_with_witnesses(
&block,
nonce,
&HashSet::new(),
&[Some(stacks.clone())],
);
assert_ne!(bare.short_ids, with_witness.short_ids);
let wtxid = crate::network::txhash::calculate_wtxid(&tx, Some(&stacks));
assert_eq!(
with_witness.short_ids,
vec![calculate_short_tx_id(&header, &wtxid, nonce)]
);
}
#[test]
fn test_reconstruct_block_empty_mempool() {
let compact_block = CompactBlock {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
},
nonce: 0,
short_ids: vec![[0u8; 6]],
prefilled_txs: vec![],
};
let mempool_txs = HashMap::new();
let missing = reconstruct_block(&compact_block, &mempool_txs).unwrap();
assert_eq!(missing.len(), 1);
}
#[test]
fn missing_short_id_keeps_the_block_index_after_a_prefilled_coinbase() {
use blvm_protocol::constants::SEQUENCE_FINAL;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{OutPoint, TransactionInput, TransactionOutput};
let tx = |prev: u8, value: i64| Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [prev; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let header = BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let block = Block {
header,
transactions: vec![tx(1, 50_000), tx(2, 40_000)].into_boxed_slice(),
};
let compact = create_compact_block(&block, 7, &HashSet::from([0]));
assert_eq!(compact.prefilled_txs[0].0, 0);
assert_eq!(compact.short_ids.len(), 1);
let missing = reconstruct_block(&compact, &HashMap::new()).unwrap();
assert_eq!(missing, vec![1]);
}
#[test]
fn prefilled_witness_survives_wire_conversion() {
use blvm_protocol::constants::SEQUENCE_FINAL;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{OutPoint, TransactionInput, TransactionOutput};
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1u8; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let block = Block {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
},
transactions: vec![tx].into_boxed_slice(),
};
let witness = vec![vec![vec![OP_1]]];
let compact = create_compact_block_with_witnesses(
&block,
7,
&HashSet::from([0]),
&[Some(witness.clone())],
);
assert_eq!(compact.prefilled_txs[0].2, Some(witness.clone()));
let message = blvm_protocol::network::CmpctBlockMessage::try_from(compact).unwrap();
assert_eq!(message.prefilled_txs[0].witness, Some(witness.clone()));
let back = blvm_protocol::bip152::CompactBlock::from(&message);
assert_eq!(back.prefilled_txs[0].2, Some(witness));
}
#[test]
fn ambiguous_short_id_is_requested() {
use blvm_protocol::constants::SEQUENCE_FINAL;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{OutPoint, TransactionInput, TransactionOutput};
let header = BlockHeader {
version: 0,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let nonce = 0u64;
let mut first = [0u8; 32];
first[24] = 0x99;
first[25] = 0x13;
first[26] = 0x4b;
let mut second = [0u8; 32];
second[24] = 0xd7;
second[25] = 0x08;
second[26] = 0x79;
let shared = calculate_short_tx_id(&header, &first, nonce);
assert_eq!(shared, calculate_short_tx_id(&header, &second, nonce));
let unique = [9u8; 32];
let unique_id = calculate_short_tx_id(&header, &unique, nonce);
assert_ne!(unique_id, shared);
let tx = |version: u64| Transaction {
version,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [version as u8; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let mut pool = HashMap::new();
pool.insert(first, tx(1));
pool.insert(second, tx(2));
pool.insert(unique, tx(3));
let compact = CompactBlock {
header,
nonce,
short_ids: vec![shared, unique_id],
prefilled_txs: vec![],
};
let missing = reconstruct_block(&compact, &pool).unwrap();
assert_eq!(missing, vec![0]);
}
#[test]
fn test_should_prefer_compact_blocks_tcp() {
assert!(!should_prefer_compact_blocks(TransportType::Tcp));
}
#[cfg(feature = "quinn")]
#[test]
fn test_should_prefer_compact_blocks_quinn() {
assert_eq!(should_prefer_compact_blocks(TransportType::Quinn), true);
}
#[cfg(feature = "iroh")]
#[test]
fn test_should_prefer_compact_blocks_iroh() {
assert_eq!(should_prefer_compact_blocks(TransportType::Iroh), true);
}
#[test]
fn test_recommended_compact_block_version_tcp() {
assert_eq!(recommended_compact_block_version(TransportType::Tcp), 1);
}
#[cfg(feature = "quinn")]
#[test]
fn test_recommended_compact_block_version_quinn() {
assert_eq!(recommended_compact_block_version(TransportType::Quinn), 2);
}
#[cfg(feature = "iroh")]
#[test]
fn test_recommended_compact_block_version_iroh() {
assert_eq!(recommended_compact_block_version(TransportType::Iroh), 2);
}
#[test]
fn test_is_quic_transport_tcp() {
assert!(!is_quic_transport(TransportType::Tcp));
}
#[cfg(feature = "quinn")]
#[test]
fn test_is_quic_transport_quinn() {
assert_eq!(is_quic_transport(TransportType::Quinn), true);
}
#[cfg(feature = "iroh")]
#[test]
fn test_is_quic_transport_iroh() {
assert_eq!(is_quic_transport(TransportType::Iroh), true);
}
#[test]
fn blocktxn_witness_fills_the_short_id_after_a_prefilled_coinbase() {
use blvm_protocol::constants::SEQUENCE_FINAL;
use blvm_protocol::opcodes::OP_1;
use blvm_protocol::{OutPoint, TransactionInput, TransactionOutput};
let tx = |prev: u8| Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [prev; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 50_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let header = BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let block = Block {
header,
transactions: vec![tx(1), tx(2)].into_boxed_slice(),
};
let compact = create_compact_block(&block, 7, &HashSet::from([0]));
let mut assembly = begin_compact_assembly(&compact, &HashMap::new()).unwrap();
assert_eq!(assembly.missing, vec![1]);
let witness = vec![vec![vec![OP_1]]];
apply_blocktxn(&mut assembly, &[tx(2)], Some(&[witness.clone()])).unwrap();
let (reconstructed, witnesses) = completed_compact_block(&assembly).unwrap();
assert_eq!(witnesses[1], witness);
assert_eq!(
reconstructed.transactions[1].outputs[0].script_pubkey,
vec![OP_1]
);
}
fn bare_compact(short_ids: Vec<ShortTxId>) -> CompactBlock {
CompactBlock {
header: BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
},
nonce: 1,
short_ids,
prefilled_txs: vec![],
}
}
fn open_budget() -> CompactBudget {
CompactBudget {
assemblies: 0,
retained_bytes: 0,
same_hash: false,
}
}
#[test]
fn a_fifth_assembly_is_fetched_whole() {
let compact = bare_compact(vec![[1u8; 6]]);
let held = decide_compact_block(
&compact,
&HashMap::new(),
CompactBudget {
assemblies: MAX_PENDING_COMPACT,
retained_bytes: 0,
same_hash: false,
},
)
.unwrap();
assert!(matches!(held, CompactDecision::FullBlock));
let replaced = decide_compact_block(
&compact,
&HashMap::new(),
CompactBudget {
assemblies: MAX_PENDING_COMPACT - 1,
retained_bytes: 0,
same_hash: true,
},
)
.unwrap();
assert!(matches!(replaced, CompactDecision::Hold(_)));
}
#[test]
fn retained_bytes_past_eight_mib_are_fetched_whole() {
let compact = bare_compact(vec![[1u8; 6]]);
let held = decide_compact_block(
&compact,
&HashMap::new(),
CompactBudget {
assemblies: 0,
retained_bytes: MAX_PENDING_COMPACT_BYTES,
same_hash: false,
},
)
.unwrap();
assert!(matches!(held, CompactDecision::FullBlock));
}
#[test]
fn a_replacement_past_the_byte_cap_keeps_the_current_assembly() {
let compact = bare_compact(vec![[1u8; 6]]);
let kept = decide_compact_block(
&compact,
&HashMap::new(),
CompactBudget {
assemblies: 0,
retained_bytes: MAX_PENDING_COMPACT_BYTES,
same_hash: true,
},
)
.unwrap();
assert!(matches!(kept, CompactDecision::KeepCurrent));
}
#[test]
fn more_than_128_holes_are_fetched_whole() {
let compact = bare_compact(vec![[2u8; 6]; MAX_COMPACT_MISSING + 1]);
let held = decide_compact_block(&compact, &HashMap::new(), open_budget()).unwrap();
assert!(matches!(held, CompactDecision::FullBlock));
let within = bare_compact(vec![[2u8; 6]; MAX_COMPACT_MISSING]);
let held = decide_compact_block(&within, &HashMap::new(), open_budget()).unwrap();
assert!(matches!(held, CompactDecision::Hold(_)));
}
#[test]
fn more_than_100_000_transactions_are_fetched_whole() {
let compact = bare_compact(vec![[3u8; 6]; MAX_COMPACT_BLOCK_TXS + 1]);
let held = decide_compact_block(&compact, &HashMap::new(), open_budget()).unwrap();
assert!(matches!(held, CompactDecision::FullBlock));
}
#[test]
fn an_index_past_u16_is_fetched_whole() {
let compact = bare_compact(vec![[4u8; 6]; (u16::MAX as usize) + 2]);
let held = decide_compact_block(&compact, &HashMap::new(), open_budget()).unwrap();
assert!(matches!(held, CompactDecision::FullBlock));
}
#[test]
fn an_assembly_older_than_sixty_seconds_is_expired() {
let compact = bare_compact(vec![[5u8; 6]]);
let CompactDecision::Hold(mut assembly) =
decide_compact_block(&compact, &HashMap::new(), open_budget()).unwrap()
else {
panic!("one hole fits the budget");
};
let now = Instant::now();
assert!(!assembly.expired_at(now));
assembly.started = now - COMPACT_ASSEMBLY_DEADLINE;
assert!(assembly.expired_at(now));
}
#[test]
fn short_id_lookup_clones_only_the_match() {
use crate::node::mempool::MempoolManager;
use blvm_protocol::constants::SEQUENCE_FINAL;
use blvm_protocol::{OutPoint, TransactionInput, TransactionOutput};
use std::sync::Arc;
let tx = |prev: u8| Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [prev; 32],
index: 0,
},
script_sig: Vec::new(),
sequence: SEQUENCE_FINAL as u64,
}]
.into(),
outputs: vec![TransactionOutput {
value: 10_000,
script_pubkey: vec![0x6a],
}]
.into(),
lock_time: 0,
};
let mempool = MempoolManager::new();
let mut policy = crate::config::MempoolPolicyConfig::default();
policy.require_standard = false;
policy.min_tx_fee = 0;
policy.min_relay_fee_rate = 0;
mempool.set_policy_config(Some(policy));
for prev in [1u8, 2u8] {
let tx = tx(prev);
let mut utxo = blvm_protocol::UtxoSet::default();
utxo.insert(
tx.inputs[0].prevout,
Arc::new(blvm_protocol::UTXO {
value: 20_000,
script_pubkey: vec![0x51].into(),
height: 1,
is_coinbase: false,
}),
);
mempool.set_utxo_set_arc(Arc::new(tokio::sync::Mutex::new(utxo)));
assert!(
mempool.add_transaction(tx).unwrap(),
"prev {prev} was not admitted"
);
}
assert_eq!(mempool.get_transactions().len(), 2);
let header = BlockHeader {
version: 1,
prev_block_hash: [0; 32],
merkle_root: [0; 32],
timestamp: 0,
bits: 0,
nonce: 0,
};
let nonce = 11u64;
let wanted = {
let txs = mempool.get_transactions();
let wtxid = crate::network::txhash::calculate_wtxid(&txs[0], None);
calculate_short_tx_id(&header, &wtxid, nonce)
};
let matched = mempool.transactions_for_short_ids(&header, nonce, &[wanted]);
assert_eq!(matched.len(), 1);
let _ = wanted;
}
}