use crate::network::txhash::calculate_wtxid;
use blvm_protocol::block::calculate_tx_id;
use blvm_protocol::{Hash, Transaction};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use tracing::{debug, info, warn};
pub struct PackageRelay {
pending_packages: HashMap<PackageId, PackageState>,
validator: PackageValidator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PackageId(pub Hash);
#[derive(Debug, Clone)]
pub struct TransactionPackage {
pub transactions: Vec<Transaction>,
pub package_id: PackageId,
pub combined_fee: u64,
pub combined_weight: usize,
}
#[derive(Debug, Clone)]
struct PackageState {
package: TransactionPackage,
received_at: u64,
status: PackageStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageStatus {
Pending,
Accepted,
Rejected { reason: PackageRejectReason },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageRejectReason {
TooManyTransactions,
WeightExceedsLimit,
FeeRateTooLow,
InvalidOrder,
DuplicateTransactions,
InvalidStructure,
}
#[derive(Debug, Clone)]
pub struct PackageValidator {
pub max_package_size: usize,
pub max_package_weight: usize,
pub min_fee_rate: u64,
}
impl Default for PackageValidator {
fn default() -> Self {
Self {
max_package_size: 25,
max_package_weight: 404_000, min_fee_rate: 1000, }
}
}
impl PackageId {
pub fn from_transactions(
transactions: &[Transaction],
witnesses: Option<&[Option<Vec<Vec<u8>>>]>,
) -> Self {
let mut wtxids: Vec<Hash> = transactions
.iter()
.enumerate()
.map(|(i, tx)| {
let w = witnesses.and_then(|w| w.get(i)).and_then(|o| o.as_ref());
match w {
Some(v) => {
let arr = [v.clone()];
calculate_wtxid(tx, Some(&arr))
}
None => calculate_wtxid(tx, None),
}
})
.collect();
wtxids.sort(); let mut hasher = Sha256::new();
for w in &wtxids {
hasher.update(w);
}
let hash_bytes = hasher.finalize();
let mut package_hash = [0u8; 32];
package_hash.copy_from_slice(&hash_bytes);
PackageId(package_hash)
}
}
impl TransactionPackage {
pub fn new(transactions: Vec<Transaction>) -> Result<Self, PackageError> {
Self::new_with_utxo_set(transactions, None)
}
pub fn new_with_utxo_set(
transactions: Vec<Transaction>,
utxo_set: Option<&blvm_protocol::UtxoSet>,
) -> Result<Self, PackageError> {
Self::new_with_utxo_set_and_witnesses(transactions, utxo_set, None)
}
pub fn new_with_utxo_set_and_witnesses(
transactions: Vec<Transaction>,
utxo_set: Option<&blvm_protocol::UtxoSet>,
witnesses: Option<&[Option<blvm_consensus::segwit::Witness>]>,
) -> Result<Self, PackageError> {
if transactions.is_empty() {
return Err(PackageError::EmptyPackage);
}
Self::validate_ordering(&transactions)?;
let package_id = PackageId::from_transactions(&transactions, witnesses);
let combined_fee = if let Some(utxo_set) = utxo_set {
transactions
.iter()
.map(|tx| {
let input_total: u64 = tx
.inputs
.iter()
.filter_map(|inp| utxo_set.get(&inp.prevout))
.map(|utxo| utxo.value as u64)
.sum();
let output_total: u64 = tx.outputs.iter().map(|out| out.value as u64).sum();
input_total.saturating_sub(output_total)
})
.sum()
} else {
0 };
let combined_weight: usize = transactions
.iter()
.enumerate()
.map(|(i, tx)| {
let witness = witnesses.and_then(|w| w.get(i)).and_then(|o| o.as_ref());
package_transaction_weight_wu(tx, witness)
})
.sum();
Ok(Self {
transactions,
package_id,
combined_fee,
combined_weight,
})
}
fn validate_ordering(transactions: &[Transaction]) -> Result<(), PackageError> {
let mut idx = std::collections::HashMap::new();
for (i, tx) in transactions.iter().enumerate() {
idx.insert(calculate_tx_id(tx), i);
}
for (i, tx) in transactions.iter().enumerate() {
for input in &tx.inputs {
if let Some(&parent_pos) = idx.get(&input.prevout.hash) {
if parent_pos >= i {
return Err(PackageError::InvalidOrder);
}
}
}
}
Ok(())
}
pub fn fee_rate(&self) -> f64 {
if self.combined_weight == 0 {
return 0.0;
}
let vbytes = self.combined_weight as f64 / 4.0;
if vbytes == 0.0 {
return 0.0;
}
self.combined_fee as f64 / vbytes
}
}
impl Default for PackageRelay {
fn default() -> Self {
Self::new()
}
}
impl PackageRelay {
pub fn new() -> Self {
Self {
pending_packages: HashMap::new(),
validator: PackageValidator::default(),
}
}
pub fn create_package(
&self,
transactions: Vec<Transaction>,
) -> Result<TransactionPackage, PackageError> {
TransactionPackage::new(transactions)
}
pub fn validate_package(
&self,
package: &TransactionPackage,
) -> Result<(), PackageRejectReason> {
if package.transactions.len() > self.validator.max_package_size {
return Err(PackageRejectReason::TooManyTransactions);
}
if package.combined_weight > self.validator.max_package_weight {
return Err(PackageRejectReason::WeightExceedsLimit);
}
if package.combined_fee > 0 {
let fee_rate = package.fee_rate();
if fee_rate < self.validator.min_fee_rate as f64 {
return Err(PackageRejectReason::FeeRateTooLow);
}
}
let mut seen = std::collections::HashSet::new();
for tx in &package.transactions {
let txid = calculate_tx_id(tx);
if !seen.insert(txid) {
return Err(PackageRejectReason::DuplicateTransactions);
}
}
TransactionPackage::validate_ordering(&package.transactions)
.map_err(|_| PackageRejectReason::InvalidOrder)?;
Ok(())
}
pub fn register_package(
&mut self,
package: TransactionPackage,
) -> Result<PackageId, PackageError> {
self.validate_package(&package)
.map_err(PackageError::ValidationFailed)?;
let package_id = package.package_id;
let tx_count = package.transactions.len();
let now = crate::utils::current_timestamp();
let state = PackageState {
package,
received_at: now,
status: PackageStatus::Pending,
};
self.pending_packages.insert(package_id, state);
debug!(
"Registered package {} with {} transactions",
hex::encode(package_id.0),
tx_count
);
Ok(package_id)
}
pub fn get_package(&self, package_id: &PackageId) -> Option<&TransactionPackage> {
self.pending_packages.get(package_id).map(|s| &s.package)
}
pub fn mark_accepted(&mut self, package_id: &PackageId) {
if let Some(state) = self.pending_packages.get_mut(package_id) {
state.status = PackageStatus::Accepted;
info!("Package {} accepted", hex::encode(package_id.0));
}
}
pub fn mark_rejected(&mut self, package_id: &PackageId, reason: PackageRejectReason) {
if let Some(state) = self.pending_packages.get_mut(package_id) {
state.status = PackageStatus::Rejected { reason };
warn!(
"Package {} rejected: {:?}",
hex::encode(package_id.0),
reason
);
}
}
pub fn cleanup_old_packages(&mut self, max_age: u64) {
let now = crate::utils::current_timestamp();
let expired: Vec<PackageId> = self
.pending_packages
.iter()
.filter(|(_, state)| now - state.received_at > max_age)
.map(|(id, _)| *id)
.collect();
for id in expired {
self.pending_packages.remove(&id);
debug!("Cleaned up expired package {}", hex::encode(id.0));
}
}
}
fn package_transaction_weight_wu(
tx: &Transaction,
witness: Option<&blvm_consensus::segwit::Witness>,
) -> usize {
use blvm_consensus::segwit::calculate_transaction_weight;
calculate_transaction_weight(tx, witness).unwrap_or(0) as usize
}
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
#[error("Empty package (no transactions)")]
EmptyPackage,
#[error("Invalid transaction ordering (children before parents)")]
InvalidOrder,
#[error("Package validation failed: {0:?}")]
ValidationFailed(PackageRejectReason),
#[error("Package not found")]
PackageNotFound,
}