synor/crates/synor-consensus/src/validation.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

591 lines
17 KiB
Rust

//! Block and transaction validation rules.
//!
//! Implements consensus rules for validating:
//! - Transaction structure and signatures
//! - Block structure and proof of work
//! - UTXO spending rules
//! - Script execution (simplified)
use crate::utxo::{UtxoEntry, UtxoError, UtxoSet};
use synor_types::{
block::{Block, BlockHeader},
transaction::{Outpoint, ScriptType, Transaction, TxInput},
Amount, Hash256,
};
use thiserror::Error;
/// Transaction validator.
pub struct TransactionValidator {
/// Maximum transaction size.
max_tx_size: usize,
/// Minimum relay fee rate (sompi per byte).
min_fee_rate: u64,
}
impl TransactionValidator {
/// Creates a new transaction validator with default settings.
pub fn new() -> Self {
TransactionValidator {
max_tx_size: crate::MAX_TRANSACTION_SIZE,
min_fee_rate: 1, // 1 sompi per byte
}
}
/// Creates a validator with custom settings.
pub fn with_settings(max_tx_size: usize, min_fee_rate: u64) -> Self {
TransactionValidator {
max_tx_size,
min_fee_rate,
}
}
/// Validates a transaction's structure (without UTXO checks).
pub fn validate_structure(&self, tx: &Transaction) -> Result<(), ValidationError> {
// Check version
if tx.version == 0 || tx.version > 2 {
return Err(ValidationError::InvalidVersion(tx.version));
}
// Coinbase specific checks
if tx.is_coinbase() {
return self.validate_coinbase_structure(tx);
}
// Must have at least one input
if tx.inputs.is_empty() {
return Err(ValidationError::NoInputs);
}
// Must have at least one output
if tx.outputs.is_empty() {
return Err(ValidationError::NoOutputs);
}
// Check for duplicate inputs
let mut seen_inputs = std::collections::HashSet::new();
for input in &tx.inputs {
if !seen_inputs.insert(input.previous_output) {
return Err(ValidationError::DuplicateInput(input.previous_output));
}
}
// Validate outputs
let mut total_output = Amount::ZERO;
for output in &tx.outputs {
// Check for zero amount (except OP_RETURN)
if output.amount == Amount::ZERO
&& output.script_pubkey.script_type != ScriptType::OpReturn
{
return Err(ValidationError::ZeroValueOutput);
}
// Check for overflow
total_output = total_output
.checked_add(output.amount)
.ok_or(ValidationError::OutputOverflow)?;
}
// Check total doesn't exceed max supply
if total_output.as_sompi() > Amount::MAX_SUPPLY {
return Err(ValidationError::ExceedsMaxSupply);
}
Ok(())
}
/// Validates coinbase transaction structure.
fn validate_coinbase_structure(&self, tx: &Transaction) -> Result<(), ValidationError> {
// Coinbase must have exactly one input
if tx.inputs.len() != 1 {
return Err(ValidationError::InvalidCoinbase(
"Must have exactly one input".to_string(),
));
}
// Input must be null outpoint
if !tx.inputs[0].is_coinbase() {
return Err(ValidationError::InvalidCoinbase(
"Input must have null outpoint".to_string(),
));
}
// Must have at least one output
if tx.outputs.is_empty() {
return Err(ValidationError::NoOutputs);
}
// Coinbase script must not be too long
if tx.inputs[0].signature_script.len() > 100 {
return Err(ValidationError::InvalidCoinbase(
"Script too long".to_string(),
));
}
Ok(())
}
/// Validates a transaction against the UTXO set.
pub fn validate_against_utxos(
&self,
tx: &Transaction,
utxo_set: &UtxoSet,
current_daa_score: u64,
) -> Result<Amount, ValidationError> {
if tx.is_coinbase() {
return Ok(Amount::ZERO); // Coinbase doesn't spend UTXOs
}
let mut total_input = Amount::ZERO;
for input in &tx.inputs {
// Get the UTXO being spent
let utxo = utxo_set
.get(&input.previous_output)
.ok_or(ValidationError::UtxoNotFound(input.previous_output))?;
// Check maturity
if !utxo.is_mature(current_daa_score) {
return Err(ValidationError::ImmatureCoinbase(input.previous_output));
}
// Verify signature (simplified - in production would execute script)
self.verify_input_script(input, &utxo)?;
total_input = total_input
.checked_add(utxo.amount())
.ok_or(ValidationError::InputOverflow)?;
}
let total_output = tx.total_output();
// Check input >= output (difference is fee)
if total_input < total_output {
return Err(ValidationError::InsufficientInputs {
input: total_input,
output: total_output,
});
}
let fee = total_input.saturating_sub(total_output);
// Check minimum fee
let tx_size = tx.weight();
let min_fee = Amount::from_sompi(tx_size * self.min_fee_rate);
if fee < min_fee {
return Err(ValidationError::InsufficientFee {
provided: fee,
required: min_fee,
});
}
Ok(fee)
}
/// Verifies an input's signature script against the UTXO.
fn verify_input_script(
&self,
input: &TxInput,
utxo: &UtxoEntry,
) -> Result<(), ValidationError> {
// Simplified verification - in production would:
// 1. Parse the signature script
// 2. Execute against the UTXO's script pubkey
// 3. Verify signatures
let script_pubkey = utxo.script_pubkey();
match script_pubkey.script_type {
ScriptType::P2PKH | ScriptType::P2pkhPqc => {
// For P2PKH, signature script should contain signature + pubkey
if input.signature_script.len() < 64 {
return Err(ValidationError::InvalidSignature(
"Signature script too short".to_string(),
));
}
// In production: extract pubkey, hash it, compare to script_pubkey.data
// Then verify signature
}
ScriptType::P2SH | ScriptType::P2shPqc => {
// For P2SH, signature script contains actual script + signatures
if input.signature_script.is_empty() {
return Err(ValidationError::InvalidSignature(
"Empty signature script".to_string(),
));
}
// In production: deserialize script, hash it, compare, execute
}
ScriptType::OpReturn => {
return Err(ValidationError::SpendingOpReturn);
}
}
Ok(())
}
}
impl Default for TransactionValidator {
fn default() -> Self {
Self::new()
}
}
/// Block validator.
pub struct BlockValidator {
/// Transaction validator.
tx_validator: TransactionValidator,
/// Maximum block mass.
max_block_mass: u64,
/// Maximum transactions per block.
max_transactions: usize,
}
impl BlockValidator {
/// Creates a new block validator.
pub fn new() -> Self {
BlockValidator {
tx_validator: TransactionValidator::new(),
max_block_mass: crate::MAX_BLOCK_MASS,
max_transactions: synor_types::block::MAX_TRANSACTIONS,
}
}
/// Validates a block header.
pub fn validate_header(&self, header: &BlockHeader) -> Result<(), ValidationError> {
// Check version
if header.version == 0 {
return Err(ValidationError::InvalidVersion(header.version as u16));
}
// Check timestamp is not too far in future
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let header_timestamp = header.timestamp.as_millis();
if header_timestamp > now + 2 * 60 * 60 * 1000 {
// 2 hours in future
return Err(ValidationError::TimestampTooFar(header_timestamp));
}
// Check parents
if header.parents.is_empty() && !header.block_id().is_zero() {
// Only genesis can have no parents
return Err(ValidationError::NoParents);
}
// Check for duplicate parents
let mut seen_parents = std::collections::HashSet::new();
for parent in &header.parents {
if !seen_parents.insert(*parent) {
return Err(ValidationError::DuplicateParent(*parent));
}
}
Ok(())
}
/// Validates proof of work.
pub fn validate_pow(
&self,
header: &BlockHeader,
target: Hash256,
) -> Result<(), ValidationError> {
let hash = header.block_id();
// Check hash is below target
if hash > target {
return Err(ValidationError::InsufficientPow { hash, target });
}
Ok(())
}
/// Validates a complete block.
pub fn validate_block(
&self,
block: &Block,
utxo_set: &UtxoSet,
expected_reward: Amount,
) -> Result<Amount, ValidationError> {
// Validate header
self.validate_header(&block.header)?;
// Check transaction count
if block.body.transactions.len() > self.max_transactions {
return Err(ValidationError::TooManyTransactions {
count: block.body.transactions.len(),
max: self.max_transactions,
});
}
// First transaction must be coinbase
if block.body.transactions.is_empty() {
return Err(ValidationError::MissingCoinbase);
}
if !block.body.transactions[0].is_coinbase() {
return Err(ValidationError::FirstTxNotCoinbase);
}
// No other transactions can be coinbase
for tx in block.body.transactions.iter().skip(1) {
if tx.is_coinbase() {
return Err(ValidationError::MultipleCoinbase);
}
}
// Validate all transactions
let mut total_fees = Amount::ZERO;
let mut total_mass = 0u64;
for (i, tx) in block.body.transactions.iter().enumerate() {
// Validate structure
self.tx_validator.validate_structure(tx)?;
// Validate against UTXOs (skip coinbase)
if i > 0 {
let fee = self.tx_validator.validate_against_utxos(
tx,
utxo_set,
block.header.daa_score,
)?;
total_fees = total_fees.saturating_add(fee);
}
// Track mass
total_mass += tx.weight();
}
// Check block mass
if total_mass > self.max_block_mass {
return Err(ValidationError::BlockMassExceeded {
mass: total_mass,
max: self.max_block_mass,
});
}
// Validate coinbase output
let coinbase = &block.body.transactions[0];
let coinbase_output = coinbase.total_output();
let max_coinbase = expected_reward.saturating_add(total_fees);
if coinbase_output > max_coinbase {
return Err(ValidationError::InvalidCoinbaseAmount {
output: coinbase_output,
max: max_coinbase,
});
}
// Verify merkle root
let computed_merkle = block.body.merkle_root();
if computed_merkle != block.header.merkle_root {
return Err(ValidationError::InvalidMerkleRoot {
expected: block.header.merkle_root,
computed: computed_merkle,
});
}
Ok(total_fees)
}
}
impl Default for BlockValidator {
fn default() -> Self {
Self::new()
}
}
/// Validation errors.
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Invalid version: {0}")]
InvalidVersion(u16),
#[error("Transaction has no inputs")]
NoInputs,
#[error("Transaction has no outputs")]
NoOutputs,
#[error("Block has no parents")]
NoParents,
#[error("Duplicate input: {0}")]
DuplicateInput(Outpoint),
#[error("Duplicate parent: {0}")]
DuplicateParent(Hash256),
#[error("Zero value output (non-OP_RETURN)")]
ZeroValueOutput,
#[error("Output amount overflow")]
OutputOverflow,
#[error("Input amount overflow")]
InputOverflow,
#[error("Total output exceeds max supply")]
ExceedsMaxSupply,
#[error("Invalid coinbase transaction: {0}")]
InvalidCoinbase(String),
#[error("UTXO not found: {0}")]
UtxoNotFound(Outpoint),
#[error("Immature coinbase: {0}")]
ImmatureCoinbase(Outpoint),
#[error("Insufficient inputs: have {input}, need {output}")]
InsufficientInputs { input: Amount, output: Amount },
#[error("Insufficient fee: provided {provided}, required {required}")]
InsufficientFee { provided: Amount, required: Amount },
#[error("Invalid signature: {0}")]
InvalidSignature(String),
#[error("Cannot spend OP_RETURN output")]
SpendingOpReturn,
#[error("Timestamp too far in future: {0}")]
TimestampTooFar(u64),
#[error("Insufficient proof of work: hash {hash}, target {target}")]
InsufficientPow { hash: Hash256, target: Hash256 },
#[error("Too many transactions: {count} (max {max})")]
TooManyTransactions { count: usize, max: usize },
#[error("Missing coinbase transaction")]
MissingCoinbase,
#[error("First transaction must be coinbase")]
FirstTxNotCoinbase,
#[error("Multiple coinbase transactions")]
MultipleCoinbase,
#[error("Block mass exceeded: {mass} (max {max})")]
BlockMassExceeded { mass: u64, max: u64 },
#[error("Invalid coinbase amount: output {output}, max {max}")]
InvalidCoinbaseAmount { output: Amount, max: Amount },
#[error("Invalid merkle root: expected {expected}, computed {computed}")]
InvalidMerkleRoot {
expected: Hash256,
computed: Hash256,
},
#[error("UTXO error: {0}")]
UtxoError(#[from] UtxoError),
}
#[cfg(test)]
mod tests {
use super::*;
use synor_types::transaction::{ScriptPubKey, SubnetworkId};
use synor_types::TxOutput;
fn make_p2pkh_output(amount: u64) -> TxOutput {
TxOutput::new(Amount::from_sompi(amount), ScriptPubKey::p2pkh(&[0u8; 32]))
}
#[test]
fn test_validate_empty_tx() {
let validator = TransactionValidator::new();
let tx = Transaction {
version: 1,
inputs: vec![],
outputs: vec![make_p2pkh_output(1000)],
lock_time: 0,
subnetwork_id: SubnetworkId::default(),
gas: 0,
payload: vec![],
};
let result = validator.validate_structure(&tx);
assert!(matches!(result, Err(ValidationError::NoInputs)));
}
#[test]
fn test_validate_no_outputs() {
let validator = TransactionValidator::new();
let tx = Transaction {
version: 1,
inputs: vec![TxInput::new(
Outpoint::new(Hash256::ZERO, 0),
vec![0u8; 100],
)],
outputs: vec![],
lock_time: 0,
subnetwork_id: SubnetworkId::default(),
gas: 0,
payload: vec![],
};
let result = validator.validate_structure(&tx);
assert!(matches!(result, Err(ValidationError::NoOutputs)));
}
#[test]
fn test_validate_invalid_version() {
let validator = TransactionValidator::new();
let tx = Transaction {
version: 0,
inputs: vec![TxInput::new(
Outpoint::new(Hash256::ZERO, 0),
vec![0u8; 100],
)],
outputs: vec![make_p2pkh_output(1000)],
lock_time: 0,
subnetwork_id: SubnetworkId::default(),
gas: 0,
payload: vec![],
};
let result = validator.validate_structure(&tx);
assert!(matches!(result, Err(ValidationError::InvalidVersion(0))));
}
#[test]
fn test_validate_valid_coinbase() {
let validator = TransactionValidator::new();
let tx = Transaction::coinbase(
vec![make_p2pkh_output(50_000_000_000)],
b"Synor Genesis".to_vec(),
);
let result = validator.validate_structure(&tx);
assert!(result.is_ok());
}
#[test]
fn test_validate_duplicate_input() {
let validator = TransactionValidator::new();
let outpoint = Outpoint::new(Hash256::blake3(b"test"), 0);
let tx = Transaction {
version: 1,
inputs: vec![
TxInput::new(outpoint, vec![0u8; 100]),
TxInput::new(outpoint, vec![0u8; 100]), // Duplicate
],
outputs: vec![make_p2pkh_output(1000)],
lock_time: 0,
subnetwork_id: SubnetworkId::default(),
gas: 0,
payload: vec![],
};
let result = validator.validate_structure(&tx);
assert!(matches!(result, Err(ValidationError::DuplicateInput(_))));
}
}