Skip to content

Commit

Permalink
refractor time-lock
Browse files Browse the repository at this point in the history
  • Loading branch information
D-Stacks committed Nov 11, 2024
1 parent 91b4c53 commit 81a49f8
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 27 deletions.
5 changes: 4 additions & 1 deletion consensus/core/src/errors/tx.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::constants::MAX_SOMPI;
use crate::subnets::SubnetworkId;
use crate::tx::TransactionOutpoint;
use crate::tx::{TimeLock, TimeLockArg, TransactionOutpoint};
use kaspa_txscript_errors::TxScriptError;
use thiserror::Error;

Expand Down Expand Up @@ -100,6 +100,9 @@ pub enum TxRuleError {
/// fee/mass RBF validation rule
#[error("fee rate per contextual mass gram is not greater than the fee rate of the replaced transaction")]
FeerateTooLow,

#[error("invalid unlock op with time lock arg {0} on time lock {1}")]
InvalidUnlockOp(TimeLockArg, TimeLock),
}

pub type TxResult<T> = std::result::Result<T, TxRuleError>;
160 changes: 160 additions & 0 deletions consensus/core/src/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use wasm_bindgen::prelude::*;
use crate::{
hashing,
subnets::{self, SubnetworkId},
constants::LOCK_TIME_THRESHOLD,
};

/// COINBASE_TRANSACTION_INDEX is the index of the coinbase transaction in every block
Expand Down Expand Up @@ -211,6 +212,93 @@ impl Transaction {
}
}



pub enum TimeLockResult{
Finalized,
NotFinalized,
Invalid,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimeLockArg{
None,
DAAScore(u64),
PastMedianTime(u64),
}

impl Display for TimeLockArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeLockArg::None => write!(f, "TimeLockArg::None"),
TimeLockArg::DAAScore(daa_unlock_time) => write!(f, "TimeLockArg::DAAScore({})", daa_unlock_time),
TimeLockArg::PastMedianTime(past_median_unlock_time) => write!(f, "TimeLockArg::PastMedianTime({})", past_median_unlock_time),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimeLock{
None,
DAAScore(u64),
PastMedianTime(u64),
}


impl TimeLock {

pub fn is_finalized(&self, time_lock_arg: &TimeLockArg) -> TimeLockResult {
match self {
TimeLock::None => TimeLockResult::Finalized,
TimeLock::DAAScore(daa_lock_time) => match time_lock_arg {
TimeLockArg::DAAScore(daa_unlock_time) => {
if *daa_unlock_time >= LOCK_TIME_THRESHOLD {
TimeLockResult::Invalid
} else if daa_unlock_time <= daa_lock_time {
TimeLockResult::Finalized
} else {
TimeLockResult::NotFinalized
}
},
_invalid => TimeLockResult::Invalid,
},
TimeLock::PastMedianTime(past_median_lock_time) => match time_lock_arg {
TimeLockArg::PastMedianTime(past_median_unlock_time) => {
if *past_median_unlock_time < LOCK_TIME_THRESHOLD {
TimeLockResult::Invalid
} else if past_median_unlock_time <= past_median_lock_time {
TimeLockResult::Finalized
} else {
TimeLockResult::NotFinalized
}
},
_invalid => TimeLockResult::Invalid,
},
}
}
}

impl From<u64> for TimeLock {
fn from(val: u64) -> Self {
match val {
0 => TimeLock::None,
val if val < LOCK_TIME_THRESHOLD => TimeLock::DAAScore(val),
val => TimeLock::PastMedianTime(val),
}
}
}

impl Display for TimeLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeLock::None => write!(f, "TimeLock::None"),
TimeLock::DAAScore(daa_lock_time) => write!(f, "TimeLock::DAAScore({})", daa_lock_time),
TimeLock::PastMedianTime(past_median_lock_time) => write!(f, "TimeLock::PastMedianTime({})", past_median_lock_time),
}
}
}


impl Transaction {
/// Determines whether or not a transaction is a coinbase transaction. A coinbase
/// transaction is a special transaction created by miners that distributes fees and block subsidy
Expand Down Expand Up @@ -244,6 +332,10 @@ impl Transaction {
self.set_mass(mass);
self
}

pub fn get_time_lock(&self) -> TimeLock {
self.lock_time.into()
}
}

impl MemSizeEstimator for Transaction {
Expand Down Expand Up @@ -691,6 +783,74 @@ mod tests {
assert_eq!(spk, spk2);
}

#[test]
fn test_time_lock_none() {
let time_lock = TimeLock::from(0);
assert_eq!(time_lock, TimeLock::None);

let mut time_lock_arg = TimeLockArg::None;
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::DAAScore(0);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::PastMedianTime(LOCK_TIME_THRESHOLD);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));
}

#[test]
fn test_time_lock_daa_score() {

let time_lock = TimeLock::from(42);
assert_eq!(time_lock, TimeLock::DAAScore(42));

let mut time_lock_arg = TimeLockArg::None;
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Invalid));

time_lock_arg = TimeLockArg::DAAScore(41);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::DAAScore(42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::DAAScore(43);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::NotFinalized));


time_lock_arg = TimeLockArg::PastMedianTime(42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Invalid));

time_lock_arg = TimeLockArg::PastMedianTime(LOCK_TIME_THRESHOLD + 42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg.clone()), TimeLockResult::Invalid));

}

#[test]
fn test_time_lock_past_median_time() {

let time_lock = TimeLock::from(LOCK_TIME_THRESHOLD + 42);
assert_eq!(time_lock, TimeLock::PastMedianTime(LOCK_TIME_THRESHOLD + 42));

let mut time_lock_arg = TimeLockArg::None;
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Invalid));

time_lock_arg = TimeLockArg::PastMedianTime(LOCK_TIME_THRESHOLD + 41);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::PastMedianTime(LOCK_TIME_THRESHOLD + 42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Finalized));

time_lock_arg = TimeLockArg::PastMedianTime(LOCK_TIME_THRESHOLD + 43);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::NotFinalized));

time_lock_arg = TimeLockArg::DAAScore(42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Invalid));

time_lock_arg = TimeLockArg::DAAScore(LOCK_TIME_THRESHOLD + 42);
assert!(matches!(time_lock.is_finalized(&time_lock_arg), TimeLockResult::Invalid));

}

// use wasm_bindgen_test::wasm_bindgen_test;
// #[wasm_bindgen_test]
// pub fn test_wasm_serde_spk_constructor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
model::stores::{ghostdag::GhostdagStoreReader, statuses::StatusesStoreReader},
processes::window::WindowManager,
};
use kaspa_consensus_core::block::Block;
use kaspa_consensus_core::{block::Block, tx::{TimeLock, TimeLockArg}};
use kaspa_database::prelude::StoreResultExtensions;
use kaspa_hashes::Hash;
use kaspa_utils::option::OptionExtensions;
Expand All @@ -27,13 +27,15 @@ impl BlockBodyProcessor {
});

for tx in block.transactions.iter() {
// quick check to avoid the expensive Lazy eval during ibd (in most cases).
if tx.lock_time != 0 {
if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, block.header.daa_score, (*pmt_res).clone()?) {
return Err(RuleError::TxInContextFailed(tx.id(), e));
};
};
if let Err(e) = self.transaction_validator.utxo_free_tx_validation(tx, match tx.get_time_lock() {
TimeLock::None => TimeLockArg::None,
TimeLock::DAAScore(_) => TimeLockArg::DAAScore(block.header.daa_score),
TimeLock::PastMedianTime(_) => TimeLockArg::PastMedianTime((*pmt_res).clone()?),
}) {
return Err(RuleError::TxInContextFailed(tx.id(), e.clone()));
}
}

Ok(())
}

Expand Down
15 changes: 12 additions & 3 deletions consensus/src/pipeline/virtual_processor/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use kaspa_consensus_core::{
header::Header,
merkle::calc_hash_merkle_root,
pruning::PruningPointsList,
tx::{MutableTransaction, Transaction},
tx::{MutableTransaction, TimeLock, TimeLockArg, Transaction},
utxo::{
utxo_diff::UtxoDiff,
utxo_view::{UtxoView, UtxoViewComposition},
Expand Down Expand Up @@ -803,7 +803,12 @@ impl VirtualStateProcessor {
args: &TransactionValidationArgs,
) -> TxResult<()> {
self.transaction_validator.validate_tx_in_isolation(&mutable_tx.tx)?;
self.transaction_validator.utxo_free_tx_validation(&mutable_tx.tx, virtual_daa_score, virtual_past_median_time)?;

self.transaction_validator.utxo_free_tx_validation(&mutable_tx.tx, match mutable_tx.tx.get_time_lock() {
TimeLock::None => TimeLockArg::None,
TimeLock::DAAScore(_) => TimeLockArg::DAAScore(virtual_daa_score),
TimeLock::PastMedianTime(_) => TimeLockArg::PastMedianTime(virtual_past_median_time),
})?;
self.validate_mempool_transaction_in_utxo_context(mutable_tx, virtual_utxo_view, virtual_daa_score, args)?;
Ok(())
}
Expand Down Expand Up @@ -892,7 +897,11 @@ impl VirtualStateProcessor {
// No need to validate the transaction in isolation since we rely on the mining manager to submit transactions
// which were previously validated through `validate_mempool_transaction_and_populate`, hence we only perform
// in-context validations
self.transaction_validator.utxo_free_tx_validation(tx, virtual_state.daa_score, virtual_state.past_median_time)?;
self.transaction_validator.utxo_free_tx_validation(tx, match tx.get_time_lock() {
TimeLock::None => TimeLockArg::None,
TimeLock::DAAScore(_) => TimeLockArg::DAAScore(virtual_state.daa_score),
TimeLock::PastMedianTime(_) => TimeLockArg::PastMedianTime(virtual_state.past_median_time),
})?;
let ValidatedTransaction { calculated_fee, .. } =
self.validate_transaction_in_utxo_context(tx, utxo_view, virtual_state.daa_score, TxValidationFlags::Full)?;
Ok(calculated_fee)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use kaspa_consensus_core::tx::Transaction;
use kaspa_consensus_core::tx::{TimeLock, TimeLockArg, TimeLockResult, Transaction};

use crate::constants::LOCK_TIME_THRESHOLD;

Expand All @@ -8,24 +8,18 @@ use super::{
};

impl TransactionValidator {
pub fn utxo_free_tx_validation(&self, tx: &Transaction, ctx_daa_score: u64, ctx_block_time: u64) -> TxResult<()> {
self.check_tx_is_finalized(tx, ctx_daa_score, ctx_block_time)
pub fn utxo_free_tx_validation(&self, tx: &Transaction, time_lock_arg: TimeLockArg) -> TxResult<()> {
self.check_tx_is_finalized(tx, time_lock_arg)
}

fn check_tx_is_finalized(&self, tx: &Transaction, ctx_daa_score: u64, ctx_block_time: u64) -> TxResult<()> {
// Lock time of zero means the transaction is finalized.
if tx.lock_time == 0 {
return Ok(());
}
fn check_tx_is_finalized(&self, tx: &Transaction, time_lock_arg: TimeLockArg) -> TxResult<()> {

// The lock time field of a transaction is either a block DAA score at
// which the transaction is finalized or a timestamp depending on if the
// value is before the LOCK_TIME_THRESHOLD. When it is under the
// threshold it is a DAA score.
let block_time_or_daa_score = if tx.lock_time < LOCK_TIME_THRESHOLD { ctx_daa_score } else { ctx_block_time };
if tx.lock_time < block_time_or_daa_score {
return Ok(());
}
let time_lock = tx.get_time_lock();
match time_lock.is_finalized(&time_lock_arg) {
TimeLockResult::Finalized => return Ok(()),
TimeLockResult::NotFinalized => (),
TimeLockResult::Invalid => return Err(TxRuleError::InvalidUnlockOp(time_lock_arg, time_lock)),
};

// At this point, the transaction's lock time hasn't occurred yet, but
// the transaction might still be finalized if the sequence number
Expand Down

0 comments on commit 81a49f8

Please sign in to comment.