Skip to content

Commit

Permalink
add: new struct for utxo: UtxoData, new trait for time functionalitie…
Browse files Browse the repository at this point in the history
…s: NodeTime.

add: get_mtp() method for Persisted ChainState.
fix: time-related validation for Persisted ChainState.
  • Loading branch information
jaoleal committed Jan 22, 2025
1 parent fac9963 commit 6bbe2c9
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 35 deletions.
5 changes: 3 additions & 2 deletions crates/floresta-chain/benches/chain_state_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use criterion::criterion_main;
use criterion::BatchSize;
use criterion::Criterion;
use criterion::SamplingMode;
use floresta_chain::pruned_utreexo::utxo_data::UtxoData;
use floresta_chain::pruned_utreexo::UpdatableChainstate;
use floresta_chain::AssumeValidArg;
use floresta_chain::ChainState;
Expand Down Expand Up @@ -44,7 +45,7 @@ fn setup_test_chain<'a>(
fn decode_block_and_inputs(
block_file: File,
stxos_file: File,
) -> (Block, HashMap<OutPoint, TxOut>) {
) -> (Block, HashMap<OutPoint, UtxoData>) {
let block_bytes = zstd::decode_all(block_file).unwrap();
let block: Block = deserialize(&block_bytes).unwrap();

Expand All @@ -58,7 +59,7 @@ fn decode_block_and_inputs(
.iter()
.skip(1) // Skip the coinbase transaction
.flat_map(|tx| &tx.input)
.map(|txin| (txin.previous_output, stxos.remove(0)))
.map(|txin| (txin.previous_output, UtxoData(stxos.remove(0), 0)))
.collect();

assert!(stxos.is_empty(), "Moved all stxos to the inputs map");
Expand Down
52 changes: 45 additions & 7 deletions crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use alloc::vec::Vec;
use core::cell::UnsafeCell;
#[cfg(feature = "bitcoinconsensus")]
use core::ffi::c_uint;
use core::ops::Div;

use bitcoin::block::Header as BlockHeader;
use bitcoin::blockdata::constants::genesis_block;
Expand All @@ -21,10 +22,8 @@ use bitcoin::hashes::Hash;
use bitcoin::script;
use bitcoin::Block;
use bitcoin::BlockHash;
use bitcoin::OutPoint;
use bitcoin::Target;
use bitcoin::Transaction;
use bitcoin::TxOut;
use bitcoin::Work;
use floresta_common::Channel;
use log::info;
Expand All @@ -44,8 +43,10 @@ use super::chainstore::KvChainStore;
use super::consensus::Consensus;
use super::error::BlockValidationErrors;
use super::error::BlockchainError;
use super::nodetime::DisableTime;
use super::partial_chain::PartialChainState;
use super::partial_chain::PartialChainStateInner;
use super::utxo_data::UtxoMap;
use super::BlockchainInterface;
use super::ChainStore;
use super::UpdatableChainstate;
Expand Down Expand Up @@ -750,8 +751,22 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
&self,
block: &Block,
height: u32,
inputs: HashMap<OutPoint, TxOut>,
inputs: UtxoMap,
) -> Result<(), BlockchainError> {
let mtp = match self.get_mtp(height) {
Ok(array) => {
let mtp = array[array.len().div(2)];

#[cfg(not(feature = "std"))]
let time = DisableTime;
#[cfg(feature = "std")]
let time = StdNodeTime;
Consensus::validate_block_time(block.header.time, mtp, time)?;
Some(mtp)
}
_ => None,
};

if !block.check_merkle_root() {
return Err(BlockchainError::BlockValidation(
BlockValidationErrors::BadMerkleRoot,
Expand All @@ -775,12 +790,14 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
// Validate block transactions
let subsidy = read_lock!(self).consensus.get_subsidy(height);
let verify_script = self.verify_script(height);

#[cfg(feature = "bitcoinconsensus")]
let flags = self.get_validation_flags(height, block.header.block_hash());
#[cfg(not(feature = "bitcoinconsensus"))]
let flags = 0;
Consensus::verify_block_transactions(
height,
mtp,
inputs,
&block.txdata,
subsidy,
Expand Down Expand Up @@ -828,7 +845,7 @@ impl<PersistedState: ChainStore> BlockchainInterface for ChainState<PersistedSta
&self,
block: &Block,
proof: Proof,
inputs: HashMap<OutPoint, TxOut>,
inputs: UtxoMap,
del_hashes: Vec<sha256::Hash>,
acc: Stump,
) -> Result<(), Self::Error> {
Expand Down Expand Up @@ -1001,6 +1018,26 @@ impl<PersistedState: ChainStore> BlockchainInterface for ChainState<PersistedSta
}
}
impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedState> {
fn get_mtp(&self, height: u32) -> Result<[u32; 11], BlockchainError> {
let mut initial_array = [0u32; 11];

for i in 0..11 {
let time = match self.get_block_hash(height.saturating_sub(i)) {
Ok(hash) => self.get_block_header(&hash)?.time,
_ => {
warn!("Block timestamp : {i} Not found");
0
}
};
initial_array[10 - i as usize] = time;
}
//This is the case where we didnt find even the block in the given height
if initial_array[10] == 0 {
return Err(BlockchainError::BlockNotPresent);
}
return Ok(initial_array);
}

fn switch_chain(&self, new_tip: BlockHash) -> Result<(), BlockchainError> {
let new_tip = self.get_block_header(&new_tip)?;
self.reorg(new_tip)
Expand Down Expand Up @@ -1071,7 +1108,7 @@ impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedSta
&self,
block: &Block,
proof: Proof,
inputs: HashMap<OutPoint, TxOut>,
inputs: UtxoMap,
del_hashes: Vec<sha256::Hash>,
) -> Result<u32, BlockchainError> {
let header = self.get_disk_block_header(&block.block_hash())?;
Expand Down Expand Up @@ -1351,6 +1388,7 @@ mod test {
use super::UpdatableChainstate;
use crate::prelude::HashMap;
use crate::pruned_utreexo::consensus::Consensus;
use crate::pruned_utreexo::utxo_data::UtxoData;
use crate::AssumeValidArg;
use crate::KvChainStore;
use crate::Network;
Expand All @@ -1367,7 +1405,7 @@ mod test {
fn decode_block_and_inputs(
block_file: File,
stxos_file: File,
) -> (Block, HashMap<OutPoint, TxOut>) {
) -> (Block, HashMap<OutPoint, UtxoData>) {
let block_bytes = zstd::decode_all(block_file).unwrap();
let block: Block = deserialize(&block_bytes).unwrap();

Expand All @@ -1381,7 +1419,7 @@ mod test {
.iter()
.skip(1) // Skip the coinbase transaction
.flat_map(|tx| &tx.input)
.map(|txin| (txin.previous_output, stxos.remove(0)))
.map(|txin| (txin.previous_output, UtxoData(stxos.remove(0), 0)))
.collect();

assert!(stxos.is_empty(), "Moved all stxos to the inputs map");
Expand Down
89 changes: 78 additions & 11 deletions crates/floresta-chain/src/pruned_utreexo/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
extern crate alloc;

use core::ffi::c_uint;
use core::ops::Sub;

use bitcoin::absolute::Height;
use bitcoin::absolute::Time;
use bitcoin::block::Header as BlockHeader;
use bitcoin::consensus::Encodable;
use bitcoin::hashes::sha256;
use bitcoin::hashes::Hash;
use bitcoin::Block;
use bitcoin::BlockHash;
use bitcoin::CompactTarget;
use bitcoin::OutPoint;
use bitcoin::ScriptBuf;
use bitcoin::Target;
use bitcoin::Transaction;
Expand All @@ -30,8 +32,13 @@ use sha2::Sha512_256;
use super::chainparams::ChainParams;
use super::error::BlockValidationErrors;
use super::error::BlockchainError;
use super::nodetime::NodeTime;
use super::nodetime::HOUR;
use super::utxo_data::UtxoMap;
use crate::TransactionError;

pub const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000_ffff;

/// The value of a single coin in satoshis.
pub const COIN_VALUE: u64 = 100_000_000;

Expand Down Expand Up @@ -110,10 +117,10 @@ impl Consensus {
/// - The transaction must not have duplicate inputs
/// - The transaction must not spend more coins than it claims in the inputs
/// - The transaction must have valid scripts
#[allow(unused)]
pub fn verify_block_transactions(
height: u32,
mut utxos: HashMap<OutPoint, TxOut>,
mtp: Option<u32>,
mut utxos: UtxoMap,
transactions: &[Transaction],
subsidy: u64,
verify_script: bool,
Expand Down Expand Up @@ -170,6 +177,17 @@ impl Consensus {
error,
}
})?;
match mtp {
Some(mtp) => {
Self::validate_locktime(input, transaction, &utxos, height, mtp).map_err(
|error| TransactionError {
txid: transaction.compute_txid(),
error,
},
)?;
}
_ => {}
}
// TODO check also witness script size
}

Expand All @@ -193,7 +211,14 @@ impl Consensus {
#[cfg(feature = "bitcoinconsensus")]
if verify_script {
transaction
.verify_with_flags(|outpoint| utxos.remove(outpoint), flags)
.verify_with_flags(
// TO-DO: Make this less horrible to understand
|outpoint| match utxos.remove(outpoint) {
Some(utxodata) => Some(utxodata.0),
_ => None,
},
flags,
)
.map_err(|err| TransactionError {
txid: transaction.compute_txid(),
error: BlockValidationErrors::ScriptValidationError(err.to_string()),
Expand Down Expand Up @@ -224,12 +249,9 @@ impl Consensus {
/// Returns the TxOut being spent by the given input.
///
/// Fails if the UTXO is not present in the given hashmap.
fn get_utxo<'a>(
input: &TxIn,
utxos: &'a HashMap<OutPoint, TxOut>,
) -> Result<&'a TxOut, BlockValidationErrors> {
fn get_utxo<'a>(input: &TxIn, utxos: &'a UtxoMap) -> Result<&'a TxOut, BlockValidationErrors> {
match utxos.get(&input.previous_output) {
Some(txout) => Ok(txout),
Some(txout) => Ok(&txout.0),
None => Err(
// This is the case when the spender:
// - Spends an UTXO that doesn't exist
Expand All @@ -238,13 +260,58 @@ impl Consensus {
),
}
}
#[allow(unused)]
pub fn validate_block_time(
block_timestamp: u32,
mtp: u32,
time: impl NodeTime,
) -> Result<(), BlockValidationErrors> {
if mtp > block_timestamp && block_timestamp > (time.get_time().sub(2 * HOUR)) {
return Err(BlockValidationErrors::InvalidBlockTimestamp);
}
Ok(())
}
/// Validate the transaction locktime.
fn validate_locktime(
input: &TxIn,
transaction: &Transaction,
out_map: &UtxoMap,
height: u32,
mtp: u32,
) -> Result<(), BlockValidationErrors> {
unimplemented!("validate_locktime")
let is_relative_locked = input.sequence.is_relative_lock_time();
if is_relative_locked {
//validate lock time
let prevout = out_map.get(&input.previous_output).unwrap();

let is_block_locked = input.sequence.is_height_locked();

if is_block_locked {
let committed_height = prevout.1;
// to retrieve the span contained in the sequence we have to just get the u32 value that the sequence contains.
let height_span = input.sequence.0 & SEQUENCE_LOCKTIME_MASK;
// if the committed height + the span is greater than the current height, the transaction is invalid.
if committed_height + height_span > height {
return Err(BlockValidationErrors::BadRelativeLockTime);
}
}
if !is_block_locked {
let committed_time = prevout.1;
// here we have to shift the sequence 16 bits to the left and 16 to the right get the time lock without the flag messing with us.
let time_lock = (input.sequence.0 & SEQUENCE_LOCKTIME_MASK) * 512_u32;
if committed_time + time_lock > mtp {
return Err(BlockValidationErrors::BadRelativeLockTime);
}
}
}
if !is_relative_locked
&& !transaction.is_absolute_timelock_satisfied(
Height::from_consensus(height).unwrap(),
Time::from_consensus(mtp).unwrap(),
)
{
return Err(BlockValidationErrors::BadAbsoluteLockTime);
}
Ok(())
}
/// Validates the script size and the number of sigops in a scriptpubkey or scriptsig.
fn validate_script_size(script: &ScriptBuf) -> Result<(), BlockValidationErrors> {
Expand Down
12 changes: 12 additions & 0 deletions crates/floresta-chain/src/pruned_utreexo/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub struct TransactionError {

#[derive(Clone, Debug, PartialEq)]
pub enum BlockValidationErrors {
InvalidBlockTimestamp,
InvalidCoinbase(String),
UtxoNotFound(OutPoint),
ScriptValidationError(String),
Expand All @@ -53,6 +54,8 @@ pub enum BlockValidationErrors {
BadBip34,
InvalidProof,
CoinbaseNotMatured,
BadRelativeLockTime,
BadAbsoluteLockTime,
}

impl Display for TransactionError {
Expand All @@ -64,6 +67,15 @@ impl Display for TransactionError {
impl Display for BlockValidationErrors {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
BlockValidationErrors::InvalidBlockTimestamp => {
write!(f, "A block contains a invalid timestamp")
}
BlockValidationErrors::BadAbsoluteLockTime => {
write!(f, "A transaction contains a invalid absolute lock time.",)
}
BlockValidationErrors::BadRelativeLockTime => {
write!(f, "A transaction contains a invalid relative lock time.",)
}
BlockValidationErrors::ScriptValidationError(e) => {
write!(f, "{}", e)
}
Expand Down
Loading

0 comments on commit 6bbe2c9

Please sign in to comment.