use crate::mempool::{
Mempool,
errors::{NonStandardError, NonStandardResult},
};
use kaspa_consensus_core::{
constants::{MAX_SCRIPT_PUBLIC_KEY_VERSION, MAX_SOMPI},
tx::MutableTransaction,
};
use kaspa_txscript::{p2sh_sig_scanner, script_class::ScriptClass};
const MAX_STANDARD_P2SH_SIG_OPS: u16 = 15;
impl Mempool {
pub(crate) fn check_transaction_standard_in_isolation(&self, transaction: &MutableTransaction) -> NonStandardResult<()> {
let transaction_id = transaction.id();
for (i, output) in transaction.tx.outputs.iter().enumerate() {
if output.script_public_key.version() > MAX_SCRIPT_PUBLIC_KEY_VERSION {
return Err(NonStandardError::RejectScriptPublicKeyVersion(transaction_id, i));
}
if ScriptClass::from_script(&output.script_public_key) == ScriptClass::NonStandard {
return Err(NonStandardError::RejectOutputScriptClass(transaction_id, i));
}
}
Ok(())
}
pub(crate) fn check_transaction_standard_in_context(&self, transaction: &MutableTransaction) -> NonStandardResult<()> {
let transaction_id = transaction.id();
for i in 0..transaction.tx.inputs.len() {
let entry = transaction.entries[i].as_ref().unwrap();
match ScriptClass::from_script(&entry.script_public_key) {
ScriptClass::NonStandard => {
return Err(NonStandardError::RejectInputScriptClass(transaction_id, i));
}
ScriptClass::PubKey => {}
ScriptClass::PubKeyECDSA => {}
ScriptClass::ScriptHash => {
let num_sig_ops = p2sh_sig_scanner(&transaction.tx.inputs[i].signature_script, &entry.script_public_key);
if num_sig_ops > MAX_STANDARD_P2SH_SIG_OPS as u64 {
return Err(NonStandardError::RejectSignatureCount(transaction_id, i, num_sig_ops, MAX_STANDARD_P2SH_SIG_OPS));
}
}
}
}
let masses = transaction.calculated_non_contextual_masses.unwrap();
let cofactors = self.config.mempool_mass_cofactors;
let normalized_transient_mass = masses.normalized_transient(&cofactors);
let fee_mass = masses.compute_mass.max(normalized_transient_mass);
let minimum_fee = self.minimum_required_transaction_relay_fee(fee_mass, self.config.minimum_relay_transaction_fee);
let fee = transaction.calculated_fee.unwrap();
if fee < minimum_fee {
return if masses.compute_mass >= normalized_transient_mass {
Err(NonStandardError::RejectInsufficientComputeFee(transaction_id, fee, minimum_fee, masses.compute_mass))
} else {
Err(NonStandardError::RejectInsufficientTransientFee(transaction_id, fee, minimum_fee, normalized_transient_mass))
};
}
Ok(())
}
fn minimum_required_transaction_relay_fee(&self, mass: u64, minimum_relay_transaction_fee: u64) -> u64 {
let mut minimum_fee = mass.saturating_mul(minimum_relay_transaction_fee) / 1000; if minimum_fee == 0 {
minimum_fee = minimum_relay_transaction_fee;
}
minimum_fee.min(MAX_SOMPI)
}
}
#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)]
mod tests {
use super::*;
use crate::{
MiningCounters,
mempool::config::{Config, DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE},
};
use kaspa_addresses::{Address, Prefix, Version};
use kaspa_consensus_core::{
config::params::Params,
constants::{MAX_TX_IN_SEQUENCE_NUM, SOMPI_PER_KASPA, TRANSIENT_BYTE_TO_MASS_FACTOR, TX_VERSION},
mass::NonContextualMasses,
network::NetworkType,
subnets::SUBNETWORK_ID_NATIVE,
tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry},
};
use kaspa_txscript::{
opcodes::codes::{OpReturn, OpTrue},
script_builder::ScriptBuilder,
};
use std::sync::Arc;
const RELAY_FEE_TEST_MASS: u64 = 500_000;
const fn default_minimum_relay_fee_for_mass(mass: u64) -> u64 {
mass * DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE / 1000
}
#[test]
fn test_calc_min_required_tx_relay_fee() {
struct Test {
name: &'static str,
size: u64,
minimum_relay_transaction_fee: u64,
want: u64,
}
let tests = [
Test {
name: "250 bytes with relay fee of 3",
size: 250,
minimum_relay_transaction_fee: 3,
want: 3,
},
Test {
name: "100 bytes with default minimum relay fee",
size: 100,
minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
want: default_minimum_relay_fee_for_mass(100),
},
Test {
name: "large relay fee test mass with default minimum relay fee",
size: RELAY_FEE_TEST_MASS,
minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
want: default_minimum_relay_fee_for_mass(RELAY_FEE_TEST_MASS),
},
Test { name: "1500 bytes with 5000 relay fee", size: 1500, minimum_relay_transaction_fee: 5000, want: 7500 },
Test { name: "1500 bytes with 3000 relay fee", size: 1500, minimum_relay_transaction_fee: 3000, want: 4500 },
Test { name: "782 bytes with 5000 relay fee", size: 782, minimum_relay_transaction_fee: 5000, want: 3910 },
Test { name: "782 bytes with 3000 relay fee", size: 782, minimum_relay_transaction_fee: 3000, want: 2346 },
Test { name: "782 bytes with 2550 relay fee", size: 782, minimum_relay_transaction_fee: 2550, want: 1994 },
];
for test in tests.iter() {
for net in NetworkType::iter() {
let params: Params = net.into();
let mut config =
Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
config.minimum_relay_transaction_fee = test.minimum_relay_transaction_fee;
let minimum_relay_transaction_fee = config.minimum_relay_transaction_fee;
let counters = Arc::new(MiningCounters::default());
let mempool = Mempool::new(Arc::new(config), counters);
let got = mempool.minimum_required_transaction_relay_fee(test.size, minimum_relay_transaction_fee);
if got != test.want {
println!("test_calc_min_required_tx_relay_fee test '{}' failed: got {}, want {}", test.name, got, test.want);
}
assert_eq!(test.want, got);
}
}
}
#[test]
fn test_check_transaction_standard_in_isolation() {
let dummy_prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1);
let dummy_sig_script = vec![0u8; 65];
let dummy_tx_input = TransactionInput::new(dummy_prev_out, dummy_sig_script, MAX_TX_IN_SEQUENCE_NUM, 1);
let addr_hash = vec![1u8; 32];
let addr = Address::new(Prefix::Testnet, Version::PubKey, &addr_hash);
let dummy_script_public_key = kaspa_txscript::pay_to_address_script(&addr);
let dummy_tx_out = TransactionOutput::new(SOMPI_PER_KASPA, dummy_script_public_key);
struct Test {
name: &'static str,
mtx: MutableTransaction,
is_standard: bool,
}
fn new_mtx(tx: Transaction, mass: u64) -> MutableTransaction {
let mut mtx = MutableTransaction::from_tx(tx);
mtx.calculated_non_contextual_masses = Some(NonContextualMasses::new(mass, mass));
mtx
}
let tests = vec![
Test {
name: "Typical pay-to-pubkey transaction",
mtx: new_mtx(
Transaction::new(
TX_VERSION,
vec![dummy_tx_input.clone()],
vec![dummy_tx_out.clone()],
0,
SUBNETWORK_ID_NATIVE,
0,
vec![],
),
1000,
),
is_standard: true,
},
Test {
name: "Transaction version above allowed",
mtx: new_mtx(
Transaction::new(
TX_VERSION + 2,
vec![dummy_tx_input.clone()],
vec![dummy_tx_out.clone()],
0,
SUBNETWORK_ID_NATIVE,
0,
vec![],
),
1000,
),
is_standard: true, },
Test {
name: "Valid but non standard public key script",
mtx: new_mtx(
Transaction::new(
TX_VERSION,
vec![dummy_tx_input.clone()],
vec![TransactionOutput::new(
SOMPI_PER_KASPA,
ScriptPublicKey::new(
MAX_SCRIPT_PUBLIC_KEY_VERSION,
ScriptBuilder::new().add_op(OpTrue).unwrap().script().into(),
),
)],
0,
SUBNETWORK_ID_NATIVE,
0,
vec![],
),
1000,
),
is_standard: false,
},
Test {
name: "Null-data transaction",
mtx: new_mtx(
Transaction::new(
TX_VERSION,
vec![dummy_tx_input],
vec![TransactionOutput::new(
SOMPI_PER_KASPA,
ScriptPublicKey::new(
MAX_SCRIPT_PUBLIC_KEY_VERSION,
ScriptBuilder::new().add_op(OpReturn).unwrap().script().into(),
),
)],
0,
SUBNETWORK_ID_NATIVE,
0,
vec![],
),
1000,
),
is_standard: false,
},
];
for test in tests {
for net in NetworkType::iter() {
let params: Params = net.into();
let config =
Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
let counters = Arc::new(MiningCounters::default());
let mempool = Mempool::new(Arc::new(config), counters);
println!("test_check_transaction_standard_in_isolation test '{}' ", test.name);
let res = mempool.check_transaction_standard_in_isolation(&test.mtx);
if res.is_ok() && test.is_standard {
continue;
}
if res.is_ok() && !test.is_standard {
println!("test_check_transaction_standard_in_isolation ({}): standard when it should not be", test.name);
}
if res.is_err() && test.is_standard {
println!(
"test_check_transaction_standard_in_isolation ({}): nonstandard when it should not be: {:?}",
test.name, res
);
}
assert_eq!(res.is_ok(), test.is_standard, "failed for test '{}': {:?}", test.name, res);
}
}
}
#[test]
fn test_check_transaction_standard_in_context() {
let addr = Address::new(Prefix::Testnet, Version::PubKey, &[1u8; 32]);
let standard_script_public_key = kaspa_txscript::pay_to_address_script(&addr);
let non_standard_script_public_key =
ScriptPublicKey::new(MAX_SCRIPT_PUBLIC_KEY_VERSION, ScriptBuilder::new().add_op(OpTrue).unwrap().script().into());
enum Expected {
Standard,
RejectInputScriptClass,
RejectInsufficientComputeFee { fee: u64, minimum_fee: u64, compute_mass: u64 },
RejectInsufficientTransientFee { fee: u64, minimum_fee: u64, normalized_transient_mass: u64 },
}
struct Test {
name: &'static str,
mtx: MutableTransaction,
expected: Expected,
}
fn new_mtx(script_public_key: ScriptPublicKey, masses: NonContextualMasses, fee: u64) -> MutableTransaction {
let prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1);
let input = TransactionInput::new(prev_out, vec![], MAX_TX_IN_SEQUENCE_NUM, 1);
let tx = Transaction::new(
TX_VERSION,
vec![input],
vec![TransactionOutput::new(SOMPI_PER_KASPA, script_public_key.clone())],
0,
SUBNETWORK_ID_NATIVE,
0,
vec![],
);
let mut mtx = MutableTransaction::with_entries(
tx.into(),
vec![UtxoEntry::new(2 * SOMPI_PER_KASPA, script_public_key, 0, false, None)],
);
mtx.calculated_non_contextual_masses = Some(masses);
mtx.calculated_fee = Some(fee);
mtx
}
let params: Params = NetworkType::Simnet.into();
let cofactors = params.block_mass_cofactors();
let transient = |bytes| bytes * TRANSIENT_BYTE_TO_MASS_FACTOR;
let normalized_transient = |bytes| NonContextualMasses::new(0, transient(bytes)).normalized_transient(&cofactors);
let bytes = 5_000;
let compute = normalized_transient(bytes);
let boundary_fee = default_minimum_relay_fee_for_mass(compute);
let insufficient_fee = boundary_fee - 1;
let tests = vec![
Test {
name: "standard input with exactly sufficient relay fee",
mtx: new_mtx(standard_script_public_key.clone(), NonContextualMasses::new(compute, transient(bytes)), boundary_fee),
expected: Expected::Standard,
},
Test {
name: "non-standard input script class",
mtx: new_mtx(
non_standard_script_public_key,
NonContextualMasses::new(1_000, 1_000),
DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
),
expected: Expected::RejectInputScriptClass,
},
Test {
name: "compute mass triggers insufficient relay fee",
mtx: new_mtx(
standard_script_public_key.clone(),
NonContextualMasses::new(compute, transient(bytes - 1)),
insufficient_fee,
),
expected: Expected::RejectInsufficientComputeFee {
fee: insufficient_fee,
minimum_fee: boundary_fee,
compute_mass: compute,
},
},
Test {
name: "transient mass triggers insufficient relay fee",
mtx: new_mtx(standard_script_public_key, NonContextualMasses::new(compute - 1, transient(bytes)), insufficient_fee),
expected: Expected::RejectInsufficientTransientFee {
fee: insufficient_fee,
minimum_fee: boundary_fee,
normalized_transient_mass: compute,
},
},
];
let config = Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
let counters = Arc::new(MiningCounters::default());
let mempool = Mempool::new(Arc::new(config), counters);
for test in tests {
let res = mempool.check_transaction_standard_in_context(&test.mtx);
match test.expected {
Expected::Standard => assert_eq!(res, Ok(()), "failed for test '{}'", test.name),
Expected::RejectInputScriptClass => {
assert_eq!(res, Err(NonStandardError::RejectInputScriptClass(test.mtx.id(), 0)), "failed for test '{}'", test.name)
}
Expected::RejectInsufficientComputeFee { fee, minimum_fee, compute_mass } => assert_eq!(
res,
Err(NonStandardError::RejectInsufficientComputeFee(test.mtx.id(), fee, minimum_fee, compute_mass)),
"failed for test '{}'",
test.name
),
Expected::RejectInsufficientTransientFee { fee, minimum_fee, normalized_transient_mass } => assert_eq!(
res,
Err(NonStandardError::RejectInsufficientTransientFee(test.mtx.id(), fee, minimum_fee, normalized_transient_mass)),
"failed for test '{}'",
test.name
),
}
}
}
}