Skip to content

Commit

Permalink
Chain: Test oversized scripts (vinteumorg#387)
Browse files Browse the repository at this point in the history
* consensus: refactor verify_block_transactions

This commit moves transaction-specific validation to a dedicated
function. This will be used to test our transaction validation logic,
and to verify mempool transactions in the future.

A new function, `verify_transaction` was created. It takes the UTXOs
map, a transaction, some flags and returns the input/output amounts or
an error, if any. It'll check the following:

  - The transaction doesn't spend more coins than it claims in the inputs
  - The transaction doesn't create more coins than allowed
  - The transaction has valid scripts
  - The transaction doesn't have duplicate inputs

* consensus: test over sized scripts

This commit adds some tests with huge scripts to see our validation is
working as expected. Those test cases were tested against core and we
check if floresta outputs the same thing as the former.
  • Loading branch information
Davidson-Souza authored and jaoleal committed Mar 7, 2025
1 parent 005189e commit 4286473
Show file tree
Hide file tree
Showing 12 changed files with 627 additions and 61 deletions.
9 changes: 5 additions & 4 deletions crates/floresta-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ rustreexo = "0.4"
sha2 = "^0.10.6"
log = "0.4"
kv = "0.24.0"
bitcoin = { version = "0.32", features = [
"serde",
], default-features = false }
bitcoin = { version = "0.32", features = ["serde"], default-features = false }
spin = "0.9.8"
core2 = { version = "0.4.0", default-features = false }
floresta-common = { path = "../floresta-common", default-features = false, features = ["std"] }
floresta-common = { path = "../floresta-common", default-features = false, features = [
"std",
] }
bitcoinconsensus = { version = "0.106.0", optional = true, default-features = false }
metrics = { path = "../../metrics", optional = true }

Expand All @@ -41,6 +41,7 @@ hex = "0.4.3"

[features]
default = []
std = []
bitcoinconsensus = ["bitcoin/bitcoinconsensus", "dep:bitcoinconsensus"]
metrics = ["dep:metrics"]

Expand Down
14 changes: 12 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,16 @@ 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 {
txout: stxos.remove(0),
commited_height: 0,
commited_time: 0,
},
)
})
.collect();

assert!(stxos.is_empty(), "Moved all stxos to the inputs map");
Expand Down
2 changes: 1 addition & 1 deletion crates/floresta-chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! All data is stored in a `ChainStore` implementation, which is generic over the
//! underlying database. See the ChainStore trait for more information. For a
//! ready-to-use implementation, see the [KvChainStore] struct.
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(any(test, feature = "std")), no_std)]

pub mod pruned_utreexo;
pub(crate) use floresta_common::prelude;
Expand Down
80 changes: 73 additions & 7 deletions crates/floresta-chain/src/pruned_utreexo/chain_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ use bitcoin::hashes::sha256;
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 @@ -43,6 +41,7 @@ use super::error::BlockValidationErrors;
use super::error::BlockchainError;
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 @@ -749,8 +748,16 @@ impl<PersistedState: ChainStore> ChainState<PersistedState> {
&self,
block: &Block,
height: u32,
inputs: HashMap<OutPoint, TxOut>,
inputs: UtxoMap,
) -> Result<(), BlockchainError> {
if let Ok(mtp) = self.get_mtp(height) {
#[cfg(not(feature = "std"))]
let time = super::nodetime::DisableTime;
#[cfg(feature = "std")]
let time = super::nodetime::standard_node_time::StdNodeTime;
Consensus::validate_block_time(block.header.time, mtp, time)?;
}

if !block.check_merkle_root() {
return Err(BlockValidationErrors::BadMerkleRoot)?;
}
Expand All @@ -772,12 +779,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,
block.header.time,
inputs,
&block.txdata,
subsidy,
Expand Down Expand Up @@ -825,7 +834,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 @@ -993,6 +1002,53 @@ impl<PersistedState: ChainStore> BlockchainInterface for ChainState<PersistedSta
let mut inner = write_lock!(self);
inner.broadcast_queue.drain(..).collect()
}

fn get_mtp(&self, height: u32) -> Result<u32, 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,
_ => {
info!("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);
}

let mut ret = initial_array
.iter()
.filter(|t| **t != 0)
.collect::<Vec<_>>();

//This reinforces how many blocks we need to have found to get a valid mtp.
//This check is actually useless and will never occur *if* we are in a valid and complete chain.
let range_remainder = (11u32).saturating_sub(height);

if range_remainder > 0 {
// If the range of blocks is less than 11, it should be the same value as height
if range_remainder != height {
return Err(BlockchainError::BlockNotPresent);
}
} else {
// Otherwise we should have found exactly 11 blocks
if ret.len() != 11 {
return Err(BlockchainError::BlockNotPresent);
}
}

ret.sort();

// At this point we have at least 1 block inside the array so we can safely
// divide the length by 2 to have its median.
Ok(*ret[ret.len() / 2])
}
}
impl<PersistedState: ChainStore> UpdatableChainstate for ChainState<PersistedState> {
fn switch_chain(&self, new_tip: BlockHash) -> Result<(), BlockchainError> {
Expand Down Expand Up @@ -1065,7 +1121,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 @@ -1345,6 +1401,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 @@ -1361,7 +1418,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 @@ -1375,7 +1432,16 @@ 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 {
txout: stxos.remove(0),
commited_height: 0,
commited_time: 0,
},
)
})
.collect();

assert!(stxos.is_empty(), "Moved all stxos to the inputs map");
Expand Down
Loading

0 comments on commit 4286473

Please sign in to comment.