diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index f248f3a5..b5e42608 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -793,7 +793,14 @@ impl ChainState { let flags = self.get_validation_flags(height); #[cfg(not(feature = "bitcoinconsensus"))] let flags = 0; - Consensus::verify_block_transactions(inputs, &block.txdata, subsidy, verify_script, flags)?; + Consensus::verify_block_transactions( + height, + inputs, + &block.txdata, + subsidy, + verify_script, + flags, + )?; Ok(()) } } diff --git a/crates/floresta-chain/src/pruned_utreexo/consensus.rs b/crates/floresta-chain/src/pruned_utreexo/consensus.rs index 02030857..6a0f6ed3 100644 --- a/crates/floresta-chain/src/pruned_utreexo/consensus.rs +++ b/crates/floresta-chain/src/pruned_utreexo/consensus.rs @@ -2,7 +2,6 @@ //! This module contains functions that are used to verify blocks and transactions, and doesn't //! assume anything about the chainstate, so it can be used in any context. //! We use this to avoid code reuse among the different implementations of the chainstate. - extern crate alloc; use core::ffi::c_uint; @@ -19,7 +18,10 @@ use bitcoin::OutPoint; use bitcoin::ScriptBuf; use bitcoin::Target; use bitcoin::Transaction; +use bitcoin::TxIn; use bitcoin::TxOut; +use bitcoin::Txid; +use bitcoin::WitnessVersion; use floresta_common::prelude::*; use rustreexo::accumulator::node_hash::NodeHash; use rustreexo::accumulator::proof::Proof; @@ -30,6 +32,7 @@ use sha2::Sha512_256; use super::chainparams::ChainParams; use super::error::BlockValidationErrors; use super::error::BlockchainError; +use crate::TransactionError; /// The value of a single coin in satoshis. pub const COIN_VALUE: u64 = 100_000_000; @@ -111,43 +114,74 @@ impl Consensus { /// - The transaction must have valid scripts #[allow(unused)] pub fn verify_block_transactions( + height: u32, mut utxos: HashMap, transactions: &[Transaction], subsidy: u64, verify_script: bool, flags: c_uint, ) -> Result<(), BlockchainError> { + // TODO: RETURN A GENERIC WRAPPER TYPE. // Blocks must contain at least one transaction if transactions.is_empty() { return Err(BlockValidationErrors::EmptyBlock.into()); } let mut fee = 0; + let mut wu: u64 = 0; // Skip the coinbase tx for (n, transaction) in transactions.iter().enumerate() { // We don't need to verify the coinbase inputs, as it spends newly generated coins - if transaction.is_coinbase() { - if n == 0 { - continue; - } - // A block must contain only one coinbase, and it should be the fist thing inside it - return Err(BlockValidationErrors::FirstTxIsnNotCoinbase.into()); + if transaction.is_coinbase() && n == 0 { + Self::verify_coinbase(transaction.clone(), n as u16).map_err(|err| { + TransactionError { + txid: transaction.txid(), + error: err, + } + }); + continue; } // Amount of all outputs - let output_value = transaction - .output - .iter() - .fold(0, |acc, tx| acc + tx.value.to_sat()); + let mut output_value = 0; + for output in transaction.output.iter() { + Self::get_out_value(output, &mut output_value).map_err(|err| TransactionError { + txid: transaction.txid(), + error: err, + }); + Self::validate_script_size(&output.script_pubkey).map_err(|err| TransactionError { + txid: transaction.txid(), + error: err, + }); + } // Amount of all inputs - let in_value = transaction.input.iter().fold(0, |acc, input| { - acc + utxos - .get(&input.previous_output) - .expect("We have all prevouts here") - .value - .to_sat() - }); + let mut in_value = 0; + for input in transaction.input.iter() { + Self::consume_utxos(input, &mut utxos, &mut in_value).map_err(|err| { + TransactionError { + txid: transaction.txid(), + error: err, + } + }); + Self::validate_script_size(&input.script_sig).map_err(|err| TransactionError { + txid: transaction.txid(), + error: err, + }); + /* Self::validate_locktime(input, transaction, height).map_err(|err| { + TransactionError { + txid: transaction.txid(), + error: err, + } + }); */ + } // Value in should be greater or equal to value out. Otherwise, inflation. if output_value > in_value { - return Err(BlockValidationErrors::NotEnoughMoney.into()); + return Err(TransactionError { + txid: transaction.txid(), + error: BlockValidationErrors::NotEnoughMoney, + } + .into()); + } + if output_value > 21_000_000 * 100_000_000 { + return Err(BlockValidationErrors::TooManyCoins.into()); } // Fee is the difference between inputs and outputs fee += in_value - output_value; @@ -156,12 +190,19 @@ impl Consensus { if verify_script { transaction .verify_with_flags(|outpoint| utxos.remove(outpoint), flags) - .map_err(|err| BlockValidationErrors::InvalidTx(alloc::format!("{:?}", err)))?; - } + .map_err(|err| TransactionError { + txid: transaction.txid(), + error: BlockValidationErrors::ScriptValidationError(err.to_string()), + }); + }; + + //checks vbytes validation + //After all the checks, we sum the transaction weight to the block weight + wu += transaction.weight().to_wu(); } - // In each block, the first transaction, and only the first, should be coinbase - if !transactions[0].is_coinbase() { - return Err(BlockValidationErrors::FirstTxIsnNotCoinbase.into()); + //checks if the block weight is fine. + if wu > 4_000_000 { + return Err(BlockValidationErrors::BlockTooBig.into()); } // Checks if the miner isn't trying to create inflation if fee + subsidy @@ -174,6 +215,81 @@ impl Consensus { } Ok(()) } + /// Consumes the UTXOs from the hashmap, and returns the value of the consumed UTXOs. + /// If we do not find the UTXO, we return an error invalidating the input that tried to + /// consume that UTXO. + fn consume_utxos( + input: &TxIn, + utxos: &mut HashMap, + value_var: &mut u64, + ) -> Result<(), BlockValidationErrors> { + match utxos.get(&input.previous_output) { + Some(prevout) => { + *value_var += prevout.value.to_sat(); + utxos.remove(&input.previous_output); + } + None => { + return Err(BlockValidationErrors::UtxoAlreadySpent( + //This is the case when the spender: + // - Spends an UTXO that doesn't exist + // - Spends an UTXO that was already spent + input.previous_output.txid, + )); + } + }; + Ok(()) + } + #[allow(unused)] + fn validate_locktime( + input: &TxIn, + transaction: &Transaction, + height: u32, + ) -> Result<(), BlockValidationErrors> { + unimplemented!("validate_locktime") + } + /// Validates the script size and the number of sigops in a script. + fn validate_script_size(script: &ScriptBuf) -> Result<(), BlockValidationErrors> { + let scriptpubkeysize = script.len(); + let is_taproot = + script.witness_version() == Some(WitnessVersion::V1) && scriptpubkeysize == 32; + if scriptpubkeysize > 520 || scriptpubkeysize < 2 && !is_taproot { + //the scriptsig size must be between 2 and 100 bytes unless is taproot + return Err(BlockValidationErrors::ScriptError); + } + if script.count_sigops() > 80_000 { + return Err(BlockValidationErrors::ScriptError); + } + Ok(()) + } + fn get_out_value(out: &TxOut, value_var: &mut u64) -> Result<(), BlockValidationErrors> { + if out.value.to_sat() > 0 { + *value_var += out.value.to_sat() + } else { + return Err(BlockValidationErrors::InvalidOutput); + } + Ok(()) + } + fn verify_coinbase(transaction: Transaction, index: u16) -> Result<(), BlockValidationErrors> { + if index != 0 { + // A block must contain only one coinbase, and it should be the fist thing inside it + return Err(BlockValidationErrors::FirstTxIsnNotCoinbase); + } + //the prevout input of a coinbase must be all zeroes + if transaction.input[0].previous_output.txid != Txid::all_zeros() { + return Err(BlockValidationErrors::InvalidCoinbase( + "Invalid coinbase txid".to_string(), + )); + } + let scriptsig = transaction.input[0].script_sig.clone(); + let scriptsigsize = scriptsig.clone().into_bytes().len(); + if !(2..=100).contains(&scriptsigsize) { + //the scriptsig size must be between 2 and 100 bytes + return Err(BlockValidationErrors::InvalidCoinbase( + "Invalid ScriptSig size".to_string(), + )); + } + Ok(()) + } /// Calculates the next target for the proof of work algorithm, given the /// current target and the time it took to mine the last 2016 blocks. pub fn calc_next_work_required( @@ -267,3 +383,148 @@ impl Consensus { false } } +#[cfg(test)] +mod tests { + use bitcoin::absolute::LockTime; + use bitcoin::hashes::sha256d::Hash; + use bitcoin::transaction::Version; + use bitcoin::Amount; + use bitcoin::OutPoint; + use bitcoin::ScriptBuf; + use bitcoin::Sequence; + use bitcoin::Transaction; + use bitcoin::TxIn; + use bitcoin::TxOut; + use bitcoin::Txid; + use bitcoin::Witness; + + use super::*; + + fn coinbase(is_valid: bool) -> Transaction { + //This coinbase transactions was retrieved from https://learnmeabitcoin.com/explorer/block/0000000000000a0f82f8be9ec24ebfca3d5373fde8dc4d9b9a949d538e9ff679 + // Create inputs + let input_txid = Txid::from_raw_hash(Hash::from_str(&format!("{:0>64}", "")).unwrap()); + + let input_vout = 0; + let input_outpoint = OutPoint::new(input_txid, input_vout); + let input_script_sig = if is_valid { + ScriptBuf::from_hex("03f0a2a4d9f0a2").unwrap() + } else { + //This should invalidate the coinbase transaction since is a big, really big, script. + ScriptBuf::from_hex(&format!("{:0>420}", "")).unwrap() + }; + + let input_sequence = Sequence::MAX; + let input = TxIn { + previous_output: input_outpoint, + script_sig: input_script_sig, + sequence: input_sequence, + witness: Witness::new(), + }; + + // Create outputs + let output_value = Amount::from_sat(5_000_350_000); + let output_script_pubkey = ScriptBuf::from_hex("41047eda6bd04fb27cab6e7c28c99b94977f073e912f25d1ff7165d9c95cd9bbe6da7e7ad7f2acb09e0ced91705f7616af53bee51a238b7dc527f2be0aa60469d140ac").unwrap(); + let output = TxOut { + value: output_value, + script_pubkey: output_script_pubkey, + }; + + // Create transaction + let version = Version(1); + let lock_time = LockTime::from_height(150_007).unwrap(); + + Transaction { + version, + lock_time, + input: vec![input], + output: vec![output], + } + } + + #[test] + fn test_validate_get_out_value() { + let output = TxOut { + value: Amount::from_sat(5_000_350_000), + script_pubkey: ScriptBuf::from_hex("41047eda6bd04fb27cab6e7c28c99b94977f073e912f25d1ff7165d9c95cd9bbe6da7e7ad7f2acb09e0ced91705f7616af53bee51a238b7dc527f2be0aa60469d140ac").unwrap(), + }; + let mut value_var = 0; + assert!(Consensus::get_out_value(&output, &mut value_var).is_ok()); + assert_eq!(value_var, 5_000_350_000); + } + + #[test] + fn test_validate_script_size() { + //the case when the script is too big + let invalid_script = ScriptBuf::from_hex(&format!("{:0>1220}", "")).unwrap(); + //the valid script < 520 bytes + let valid_script = + ScriptBuf::from_hex("76a9149206a30c09cc853bb03bd917a4f9f29b089c1bc788ac").unwrap(); + assert!(Consensus::validate_script_size(&valid_script).is_ok()); + assert!(Consensus::validate_script_size(&invalid_script).is_err()); + } + + #[test] + fn test_validate_coinbase() { + let valid_one = coinbase(true); + let invalid_one = coinbase(false); + //The case that should be valid + assert!(Consensus::verify_coinbase(valid_one.clone(), 0).is_ok()); + //Coinbase at wrong index + assert_eq!( + Consensus::verify_coinbase(valid_one, 1) + .unwrap_err() + .to_string(), + "The first transaction in a block isn't a coinbase" + ); + //Invalid coinbase script + assert_eq!( + Consensus::verify_coinbase(invalid_one, 0) + .unwrap_err() + .to_string(), + "Invalid coinbase: \"Invalid ScriptSig size\"" + ); + } + #[test] + fn test_consume_utxos() { + // Transaction extracted from https://learnmeabitcoin.com/explorer/tx/0094492b6f010a5e39c2aacc97396ce9b6082dc733a7b4151ccdbd580f789278 + // Mock data for testing + + let mut utxos = HashMap::new(); + let outpoint1 = OutPoint::new( + Txid::from_raw_hash( + Hash::from_str("5baf640769ebdf2b79868d0a259db69a2c1587232f83ba226ecf3dd0737759bd") + .unwrap(), + ), + 1, + ); + let input = TxIn { + previous_output: outpoint1, + script_sig: ScriptBuf::from_hex("493046022100841d4f503f44dd6cef8781270e7260db73d0e3c26c4f1eea61d008760000b01e022100bc2675b8598773984bcf0bb1a7cad054c649e8a34cb522a118b072a453de1bf6012102de023224486b81d3761edcd32cedda7cbb30a4263e666c87607883197c914022").unwrap(), + sequence: Sequence::MAX, + witness: Witness::new(), + }; + let prevout = TxOut { + value: Amount::from_sat(18000000), + script_pubkey: ScriptBuf::from_hex( + "76a9149206a30c09cc853bb03bd917a4f9f29b089c1bc788ac", + ) + .unwrap(), + }; + + utxos.insert(outpoint1, prevout.clone()); + + // Test consuming UTXOs + let mut value_var: u64 = 0; + assert!(Consensus::consume_utxos(&input, &mut utxos, &mut value_var).is_ok()); + assert_eq!(value_var, prevout.value.to_sat()); + + // Test double consuming UTXOs + assert_eq!( + Consensus::consume_utxos(&input, &mut utxos, &mut value_var) + .unwrap_err() + .to_string(), + "Utxo 0x5baf640769ebdf2b79868d0a259db69a2c1587232f83ba226ecf3dd0737759bd already spent" + ); + } +} diff --git a/crates/floresta-chain/src/pruned_utreexo/error.rs b/crates/floresta-chain/src/pruned_utreexo/error.rs index 93f143e6..5806f003 100644 --- a/crates/floresta-chain/src/pruned_utreexo/error.rs +++ b/crates/floresta-chain/src/pruned_utreexo/error.rs @@ -1,6 +1,7 @@ use core::fmt::Debug; use bitcoin::blockdata::script; +use bitcoin::Txid; #[cfg(feature = "cli-blockchain")] use btcd_rpc::error::UtreexodError; use floresta_common::impl_error_from; @@ -15,6 +16,7 @@ pub enum BlockchainError { JsonRpcError(#[from] UtreexodError), Parsing(String), BlockValidation(BlockValidationErrors), + TransactionError(TransactionError), InvalidProof, UtreexoError(String), Database(Box), @@ -24,10 +26,21 @@ pub enum BlockchainError { ScriptValidationFailed(script::Error), Io(ioError), } +#[derive(Clone, Debug, PartialEq)] +pub struct TransactionError { + pub txid: Txid, + pub error: BlockValidationErrors, +} #[derive(Clone, Debug, PartialEq)] pub enum BlockValidationErrors { - InvalidTx(String), + InvalidCoinbase(String), + UtxoAlreadySpent(Txid), + ScriptValidationError(String), + InvalidOutput, + ScriptError, + BlockTooBig, + TooManyCoins, NotEnoughPow, BadMerkleRoot, BadWitnessCommitment, @@ -41,11 +54,34 @@ pub enum BlockValidationErrors { CoinbaseNotMatured, } +impl Display for TransactionError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Transaction {} is invalid: {}", self.txid, self.error) + } +} + impl Display for BlockValidationErrors { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - BlockValidationErrors::InvalidTx(e) => { - write!(f, "This block contains an invalid transaction {}", e) + BlockValidationErrors::ScriptValidationError(e) => { + write!(f, "{}", e) + } + BlockValidationErrors::UtxoAlreadySpent(utxo) => { + write!(f, "Utxo {:?} already spent", utxo) + } + BlockValidationErrors::InvalidOutput => { + write!(f, "Invalid output, verify spending values") + } + BlockValidationErrors::BlockTooBig => write!(f, "Block too big"), + BlockValidationErrors::InvalidCoinbase(e) => { + write!(f, "Invalid coinbase: {:?}", e) + } + BlockValidationErrors::TooManyCoins => write!(f, "Moving more coins that exists"), + BlockValidationErrors::ScriptError => { + write!( + f, + "Script does not follow size requirements of 2>= and <=520" + ) } BlockValidationErrors::NotEnoughPow => { write!(f, "This block doesn't have enough proof-of-work") @@ -83,6 +119,7 @@ impl From for BlockchainError { } impl_error_from!(BlockchainError, ioError, Io); +impl_error_from!(BlockchainError, TransactionError, TransactionError); impl_error_from!( BlockchainError, bitcoin::consensus::encode::Error, diff --git a/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs b/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs index db0a081e..257a3447 100644 --- a/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs +++ b/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs @@ -241,7 +241,14 @@ impl PartialChainStateInner { let flags = self.get_validation_flags(height); #[cfg(not(feature = "bitcoinconsensus"))] let flags = 0; - Consensus::verify_block_transactions(inputs, &block.txdata, subsidy, verify_script, flags)?; + Consensus::verify_block_transactions( + height, + inputs, + &block.txdata, + subsidy, + verify_script, + flags, + )?; Ok(()) } } diff --git a/crates/floresta-wire/src/p2p_wire/running_node.rs b/crates/floresta-wire/src/p2p_wire/running_node.rs index a795bbc3..6729631e 100644 --- a/crates/floresta-wire/src/p2p_wire/running_node.rs +++ b/crates/floresta-wire/src/p2p_wire/running_node.rs @@ -695,8 +695,14 @@ where // it if the proof is invalid. Any other error should cause the block // to be invalidated. match e { - BlockValidationErrors::InvalidTx(_) + BlockValidationErrors::InvalidCoinbase(_) + | BlockValidationErrors::UtxoAlreadySpent(_) + | BlockValidationErrors::ScriptValidationError(_) + | BlockValidationErrors::InvalidOutput + | BlockValidationErrors::ScriptError + | BlockValidationErrors::BlockTooBig | BlockValidationErrors::NotEnoughPow + | BlockValidationErrors::TooManyCoins | BlockValidationErrors::BadMerkleRoot | BlockValidationErrors::BadWitnessCommitment | BlockValidationErrors::NotEnoughMoney diff --git a/crates/floresta-wire/src/p2p_wire/sync_node.rs b/crates/floresta-wire/src/p2p_wire/sync_node.rs index 90cedec3..fce964e9 100644 --- a/crates/floresta-wire/src/p2p_wire/sync_node.rs +++ b/crates/floresta-wire/src/p2p_wire/sync_node.rs @@ -189,8 +189,14 @@ where // it if the proof is invalid. Any other error should cause the block // to be invalidated. match e { - BlockValidationErrors::InvalidTx(_) + BlockValidationErrors::InvalidCoinbase(_) + | BlockValidationErrors::UtxoAlreadySpent(_) + | BlockValidationErrors::ScriptValidationError(_) + | BlockValidationErrors::InvalidOutput + | BlockValidationErrors::ScriptError + | BlockValidationErrors::BlockTooBig | BlockValidationErrors::NotEnoughPow + | BlockValidationErrors::TooManyCoins | BlockValidationErrors::BadMerkleRoot | BlockValidationErrors::BadWitnessCommitment | BlockValidationErrors::NotEnoughMoney