From 5ead3e840aad7d313fb28894317e43194662785d Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Tue, 28 Jan 2025 14:43:57 -0300 Subject: [PATCH] Mempool: make proving work, add a bunch of tests Now, if you call `get_block_proof` for a block whose outputs we have in our mempool, we'll get a proof for it. This commit also adds some tests for mempool stuff --- .../src/pruned_utreexo/udata.rs | 22 + crates/floresta-wire/src/p2p_wire/mempool.rs | 630 +++++++++++++++--- .../src/p2p_wire/running_node.rs | 2 +- 3 files changed, 545 insertions(+), 109 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/udata.rs b/crates/floresta-chain/src/pruned_utreexo/udata.rs index 5d02fc50..c425592f 100644 --- a/crates/floresta-chain/src/pruned_utreexo/udata.rs +++ b/crates/floresta-chain/src/pruned_utreexo/udata.rs @@ -316,6 +316,26 @@ pub mod proof_util { EmptyStack, } + pub fn get_script_type(script: &ScriptBuf) -> ScriptPubkeyType { + if script.is_p2pkh() { + return ScriptPubkeyType::PubKeyHash; + } + + if script.is_p2sh() { + return ScriptPubkeyType::ScriptHash; + } + + if script.is_p2wpkh() { + return ScriptPubkeyType::WitnessV0PubKeyHash; + } + + if script.is_p2wsh() { + return ScriptPubkeyType::WitnessV0ScriptHash; + } + + ScriptPubkeyType::Other(script.to_bytes().into_boxed_slice()) + } + pub fn reconstruct_leaf_data( leaf: &CompactLeafData, input: &TxIn, @@ -378,6 +398,8 @@ pub mod proof_util { .expect("parent_hash: Engines shouldn't be Err") } + /// From a block, gets the roots that will be included on the acc, certifying + /// that any utxo will not be spend in the same block. pub fn get_block_adds( block: &Block, height: u32, diff --git a/crates/floresta-wire/src/p2p_wire/mempool.rs b/crates/floresta-wire/src/p2p_wire/mempool.rs index ae744860..8a2fdfea 100644 --- a/crates/floresta-wire/src/p2p_wire/mempool.rs +++ b/crates/floresta-wire/src/p2p_wire/mempool.rs @@ -4,7 +4,6 @@ //! Once our transaction is included in a block, we remove it from the mempool. use std::collections::BTreeSet; use std::collections::HashMap; -use std::collections::HashSet; use std::time::Duration; use std::time::Instant; @@ -37,6 +36,11 @@ use rustreexo::accumulator::proof::Proof; type ShortTxid = u64; #[derive(Debug)] +/// A transaction in the mempool. +/// +/// This struct holds the transaction itself, the time when we added it to the mempool, the +/// transactions that depend on it, and the transactions that it depends on. We need those extra +/// informations to make decisions when to include or not a transaction in mempool or in a block. struct MempoolTransaction { transaction: Transaction, time: Instant, @@ -44,6 +48,16 @@ struct MempoolTransaction { children: Vec, } +pub trait BlockHashOracle { + fn get_block_hash(&self, height: u32) -> Option; +} + +impl BlockHashOracle for T { + fn get_block_hash(&self, height: u32) -> Option { + self.get_block_hash(height).ok() + } +} + /// Holds the transactions that we broadcasted and are still in the mempool. #[derive(Debug)] pub struct Mempool { @@ -70,6 +84,7 @@ pub struct Mempool { prevouts: HashMap, /// A queue of transaction we know about, but don't have a proof for queue: Vec, + /// A hasher that we use to compute the short transaction ids. hasher: ahash::RandomState, } @@ -84,18 +99,36 @@ pub enum AcceptToMempoolError { InvalidPrevout, /// Memory usage is too high. MemoryUsageTooHigh, + /// We couldn't find a prevout in the mempool. + /// + /// This error only happens when we try to add a transaction without a proof, and we don't have + /// the prevouts in the mempool. PrevoutNotFound, + /// The transaction is conflicting with another transaction in the mempool. ConflictingTransaction, + /// An error happened while trying to get a proof from the accumulator. Rustreexo(String), + /// The transaction has duplicate inputs. + DuplicateInput, + BlockNotFound, } +#[derive(Debug, Clone, PartialEq, Eq)] +/// A proof for a transaction in the mempool. pub struct MempoolProof { + /// The actual utreexo proof pub proof: Proof, + /// The target hashes that we are trying to prove. pub target_hashes: Vec, + /// The leaf data for the targets we are proving pub leaves: Vec, } impl Mempool { + /// Creates a new mempool with a given maximum size and accumulator. + /// + /// The acculator should have the same roots as the one inside our chainstate, or we won't be + /// able to validate proofs. pub fn new(acc: Pollard, max_mempool_size: usize) -> Mempool { let a = rand::random(); let b = rand::random(); @@ -134,6 +167,11 @@ impl Mempool { .collect() } + /// Returns the data of the prevouts that are being spent by a transaction. + /// + /// This data isn't part of the actual transaction, usually we would fetch it from the UTXO + /// set, but we don't have one. Instead, we keep track of the prevouts that are being spent by + /// transactions in the mempool and use this method to get the data. pub fn get_prevouts(&self, tx: &Transaction) -> Vec { tx.input .iter() @@ -148,10 +186,14 @@ impl Mempool { .collect() } + /// Proves that a mempool transaction is valid for the latest accumulator state. + /// + /// This should return a proof that the transaction is valid, and the data for the prevouts + /// that are being spent by the transaction. pub fn try_prove( &self, tx: &Transaction, - chain: &impl BlockchainInterface, + block_hash: &impl BlockHashOracle, ) -> Result { let mut target_hashes = Vec::new(); let mut leaves = Vec::new(); @@ -161,7 +203,8 @@ impl Mempool { .get(&input.previous_output) .ok_or(AcceptToMempoolError::PrevoutNotFound)?; - let block_hash = chain.get_block_hash(prevout.header_code >> 1).unwrap(); + // The block hash of the block that commited the prevout. + let block_hash = block_hash.get_block_hash(prevout.header_code >> 1).unwrap(); let leaf_data: LeafData = proof_util::reconstruct_leaf_data(prevout, input, block_hash) .map_err(|_| AcceptToMempoolError::InvalidPrevout)?; @@ -186,7 +229,7 @@ impl Mempool { /// Returns a list of transactions that are in the mempool up to the block weight limit. /// - /// You can use this method to produce a template and mine a block. + /// Returns a candidate block to be mined. pub fn get_block_template( &self, version: Version, @@ -209,7 +252,7 @@ impl Mempool { } size += tx_size; - let short_txid = self.hasher.hash_one(&tx.transaction.compute_txid()); + let short_txid = self.hasher.hash_one(tx.transaction.compute_txid()); self.add_transaction_to_block(&mut txs, short_txid); } @@ -229,7 +272,9 @@ impl Mempool { block } - pub fn add_transaction_to_block( + /// Utility method that grabs one transaction and all its dependencies, then adds them to a tx + /// list. + fn add_transaction_to_block( &self, block_transactions: &mut Vec, short_txid: ShortTxid, @@ -250,39 +295,111 @@ impl Mempool { /// Consume a block and remove all transactions that were included in it. /// - /// This method will remove all transactions that were included in the block from the mempool, - /// update the local accumulator and return the transactions that were included in the block. + /// This method will remove all transactions that is in the block from the mempool, + /// update the local accumulator and return the txids that were included in the block. pub fn consume_block( &mut self, block: &Block, proof: Proof, adds: &[PollardAddition], del_hashes: &[BitcoinNodeHash], - ) -> Result, String> { - if self.transactions.is_empty() { - return Ok(Vec::new()); - } + block_height: u32, + remember_all: bool, + ) -> Result, AcceptToMempoolError> { + self.acc + .modify(adds, del_hashes, proof) + .map_err(AcceptToMempoolError::Rustreexo)?; + + if remember_all { + // add the newly created UTXOs to the prevouts + for tx in block.txdata.iter() { + let is_coinbase = tx.is_coinbase(); + for (vout, output) in tx.output.iter().enumerate() { + let leaf_data = CompactLeafData { + amount: output.value.to_sat(), + spk_ty: proof_util::get_script_type(&output.script_pubkey), + header_code: (block_height << 1) | is_coinbase as u32, + }; + let prevout = OutPoint { + txid: tx.compute_txid(), + vout: vout as u32, + }; + self.prevouts.insert(prevout, leaf_data); + } - self.acc.modify(adds, del_hashes, proof)?; + for input in tx.input.iter() { + if self.prevouts.remove(&input.previous_output).is_none() { + return Err(AcceptToMempoolError::PrevoutNotFound); + } + } + } + } Ok(block .txdata .iter() - .filter_map(|tx| { - let short_txid = self.hasher.hash_one(&tx.compute_txid()); + .map(|tx| { + let short_txid = self.hasher.hash_one(tx.compute_txid()); self.transactions .remove(&short_txid) - .map(|tx| tx.transaction) + .map(|tx| tx.transaction); + + tx.compute_txid() }) .collect()) } + /// Proves all transactions included in a block. + pub fn get_block_proof( + &self, + block: &Block, + get_block_hash: impl BlockHashOracle, + ) -> Result { + let (del_hashes, leaves): (Vec<_>, Vec<_>) = block + .txdata + .iter() + .flat_map(|tx| { + tx.input + .iter() + .flat_map(|input| { + let prevout = self + .prevouts + .get(&input.previous_output) + .ok_or(AcceptToMempoolError::PrevoutNotFound)?; + + let block_height = prevout.header_code >> 1; + let block_hash = get_block_hash + .get_block_hash(block_height) + .ok_or(AcceptToMempoolError::BlockNotFound)?; + let node_hash = BitcoinNodeHash::Some( + proof_util::reconstruct_leaf_data(prevout, input, block_hash) + .unwrap() + ._get_leaf_hashes() + .to_byte_array(), + ); + + Ok::<_, AcceptToMempoolError>((node_hash, prevout.clone())) + }) + .collect::>() + }) + .unzip(); - pub fn get_block_proof(&self, del_hashes: &[BitcoinNodeHash]) -> Result { - self.acc.batch_proof(del_hashes) + let proof = self + .acc + .batch_proof(&del_hashes) + .map_err(AcceptToMempoolError::Rustreexo)?; + + Ok(MempoolProof { + proof, + target_hashes: del_hashes, + leaves, + }) } + /// Checks if a outpoint is already spent in the mempool. + /// + /// This can be used to find conficts before adding a transaction to the mempool. fn is_already_spent(&self, outpoint: &OutPoint) -> bool { - let short_txid = self.hasher.hash_one(&outpoint.txid); + let short_txid = self.hasher.hash_one(outpoint.txid); let Some(tx) = self.transactions.get(&short_txid) else { return false; }; @@ -299,27 +416,83 @@ impl Mempool { }) } - pub fn accept_to_mempool_no_acc(&mut self, transaction: Transaction) -> Result<(), AcceptToMempoolError> { + /// Performs some very basic sanity checks on a transaction before adding it to the mempool. + /// + /// This method checks if the transaction doesn't have conflicting inputs, if it doesn't spend + /// the same output twice, and if it doesn't exceed the memory usage limit. + /// + /// TODO: Move this to floresta-wire + fn sanity_check_transaction( + &self, + transaction: &Transaction, + ) -> Result<(), AcceptToMempoolError> { let tx_size = transaction.total_size(); - self.mempool_size += tx_size; + if self.mempool_size + tx_size > self.max_mempool_size { + return Err(AcceptToMempoolError::MemoryUsageTooHigh); + } - let short_txid = self.hasher.hash_one(&transaction.compute_txid()); - let depends = self.find_mempool_depends(&transaction); - // check for duplicate inputs - let inputs = transaction.input.iter().map(|input| input.previous_output).collect::>(); + let inputs = transaction + .input + .iter() + .map(|input| input.previous_output) + .collect::>(); + if inputs.len() != transaction.input.len() { - return Err(AcceptToMempoolError::ConflictingTransaction); + return Err(AcceptToMempoolError::DuplicateInput); } - for depend in depends.iter() { - // check if the input is already spent - for input in transaction.input.iter() { - if self.is_already_spent(&input.previous_output) { - return Err(AcceptToMempoolError::ConflictingTransaction); - } + for input in transaction.input.iter() { + if self.is_already_spent(&input.previous_output) { + return Err(AcceptToMempoolError::ConflictingTransaction); } + } + + Ok(()) + } + + /// Internal utility to add a transaction to the mempool. + /// + /// This method should never be called for transactions coming from the wire, since it doesn't + /// check if the transaction is valid other than basic constraint checks. This method is used + /// by the mempool itself to add transactions that are already known to be valid, such as + /// wallet transactions. For transactions coming from the wire, use `accept_to_mempool`. + pub fn accept_to_mempool_no_acc( + &mut self, + transaction: Transaction, + ) -> Result<(), AcceptToMempoolError> { + let tx_size = transaction.total_size(); + let short_txid = self.hasher.hash_one(transaction.compute_txid()); + let depends = self.find_mempool_depends(&transaction); + if self.transactions.contains_key(&short_txid) { + return Ok(()); + } + + if self.mempool_size + tx_size > self.max_mempool_size { + return Err(AcceptToMempoolError::MemoryUsageTooHigh); + } + + self.mempool_size += tx_size; + + // this function should only be called if it spends unconfirmed outputs + // Check if the inputs are actually in the mempool + for input in transaction.input.iter() { + if self.prevouts.contains_key(&input.previous_output) { + continue; + } + + let short_txid = self.hasher.hash_one(input.previous_output.txid); + if self.transactions.contains_key(&short_txid) { + continue; + } + + return Err(AcceptToMempoolError::PrevoutNotFound); + } + + self.sanity_check_transaction(&transaction)?; + + for depend in depends.iter() { let tx = self.transactions.get_mut(depend).unwrap(); tx.children.push(short_txid); } @@ -337,11 +510,12 @@ impl Mempool { Ok(()) } + /// From a transaction that is already in the mempool, computes which transaction it depends. fn find_mempool_depends(&self, tx: &Transaction) -> Vec { tx.input .iter() .filter_map(|input| { - let short_txid = self.hasher.hash_one(&input.previous_output.txid); + let short_txid = self.hasher.hash_one(input.previous_output.txid); self.transactions.get(&short_txid).map(|_| short_txid) }) .collect() @@ -357,6 +531,11 @@ impl Mempool { remembers: &[u64], ) -> Result<(), AcceptToMempoolError> { let tx_size = transaction.total_size(); + let short_txid = self.hasher.hash_one(transaction.compute_txid()); + + if self.transactions.contains_key(&short_txid) { + return Ok(()); + } if self.mempool_size + tx_size > self.max_mempool_size { return Err(AcceptToMempoolError::MemoryUsageTooHigh); @@ -368,7 +547,6 @@ impl Mempool { self.prevouts.extend(prevouts.iter().cloned()); - let short_txid = self.hasher.hash_one(&transaction.compute_txid()); let depends = self.find_mempool_depends(&transaction); for depend in depends.iter() { @@ -379,8 +557,9 @@ impl Mempool { } } - let tx = self.transactions.get_mut(depend).unwrap(); - tx.children.push(short_txid); + self.transactions.entry(*depend).and_modify(|tx| { + tx.children.push(short_txid); + }); } self.transactions.insert( @@ -393,8 +572,6 @@ impl Mempool { }, ); - - Ok(()) } @@ -404,16 +581,15 @@ impl Mempool { self.transactions.get(&id).map(|tx| &tx.transaction) } - /// Get all transactions that were in the mempool for more than 1 hour. + /// Get all transactions that were in the mempool for more than 1 hour, if any pub fn get_stale(&mut self) -> Vec { self.transactions .iter() .filter_map(|(_, tx)| { let txid = tx.transaction.compute_txid(); - if tx.time.elapsed() > Duration::from_secs(3600) { - Some(txid) - } else { - None + match tx.time.elapsed() > Duration::from_secs(3600) { + true => Some(txid), + false => None, } }) .collect() @@ -422,19 +598,56 @@ impl Mempool { #[cfg(test)] mod tests { + use std::collections::HashMap; use std::collections::HashSet; - use bitcoin::{absolute, block, hashes::Hash, transaction::Version, CompactTarget, OutPoint, Sequence, Target, Transaction, Witness}; + use std::str::FromStr; + + use bitcoin::absolute; + use bitcoin::block; + use bitcoin::consensus::encode::deserialize_hex; + use bitcoin::hashes::Hash; + use bitcoin::transaction::Version; + use bitcoin::Block; + use bitcoin::BlockHash; + use bitcoin::OutPoint; + use bitcoin::Script; + use bitcoin::ScriptBuf; + use bitcoin::Sequence; + use bitcoin::Target; + use bitcoin::Transaction; + use bitcoin::Witness; + use floresta_chain::proof_util; + use floresta_chain::CompactLeafData; + use floresta_chain::LeafData; + use rand::Rng; + use rand::SeedableRng; + use rustreexo::accumulator::node_hash::BitcoinNodeHash; + use rustreexo::accumulator::pollard::Pollard; + use rustreexo::accumulator::pollard::PollardAddition; + use rustreexo::accumulator::proof::Proof; + + use super::BlockHashOracle; + use super::Mempool; + use crate::mempool::MempoolProof; + + struct BlockHashProvider { + block_hash: HashMap, + } + + impl BlockHashOracle for BlockHashProvider { + fn get_block_hash(&self, height: u32) -> Option { + self.block_hash.get(&height).cloned() + } + } + /// builds a list of transactions in a pseudo-random way /// /// We use those transactions in mempool tests - use rand::{Rng, SeedableRng}; - use rustreexo::accumulator::pollard::Pollard; - fn build_transactions(seed: u64, conflict: bool) -> Vec { let mut rng = rand::rngs::StdRng::seed_from_u64(seed); let mut transactions = Vec::new(); - - let n = rng.gen_range(1..10_000); + + let n = rng.gen_range(1..1_000); let mut outputs = Vec::new(); for _ in 0..n { @@ -444,7 +657,7 @@ mod tests { input: Vec::new(), output: Vec::new(), }; - + let inputs = rng.gen_range(1..10); for _ in 0..inputs { if outputs.is_empty() { @@ -452,9 +665,9 @@ mod tests { } let index = rng.gen_range(0..outputs.len()); - let previous_output: OutPoint = match conflict { + let previous_output: OutPoint = match conflict { false => outputs.remove(index), - true => outputs.get(index).unwrap().clone(), + true => *outputs.get(index).unwrap(), }; let input = bitcoin::TxIn { @@ -466,16 +679,16 @@ mod tests { tx.input.push(input); } - + let n = rng.gen_range(1..10); - + for _ in 0..n { let script = rng.gen::<[u8; 32]>(); let output = bitcoin::TxOut { value: bitcoin::Amount::from_sat(rng.gen_range(0..100_000_000)), script_pubkey: bitcoin::Script::from_bytes(&script).into(), }; - + tx.output.push(output); } @@ -483,23 +696,122 @@ mod tests { txid: tx.compute_txid(), vout: vout as u32, })); - + transactions.push(tx); } transactions } - + + #[test] + fn test_block_proof() { + let mut mempool = super::Mempool::new( + rustreexo::accumulator::pollard::Pollard::default(), + 10_000_000, + ); + + let coinbase_spk: ScriptBuf = Script::from_bytes(&[0x6a]).into(); + + let coinbase = bitcoin::Transaction { + version: Version::ONE, + lock_time: absolute::LockTime::from_consensus(0), + input: Vec::new(), + output: vec![bitcoin::TxOut { + value: bitcoin::Amount::from_sat(50_000_000), + script_pubkey: coinbase_spk.clone(), + }], + }; + + let coinbase_id = coinbase.compute_txid(); + + let block = Block { + header: bitcoin::block::Header { + version: bitcoin::block::Version::ONE, + prev_blockhash: bitcoin::BlockHash::all_zeros(), + merkle_root: bitcoin::TxMerkleNode::all_zeros(), + time: 0, + bits: bitcoin::CompactTarget::from_consensus(0x1d00ffff), + nonce: 0, + }, + txdata: vec![coinbase], + }; + + let coinbase_out_leaf = LeafData { + prevout: OutPoint { + txid: coinbase_id, + vout: 0, + }, + utxo: bitcoin::TxOut { + value: bitcoin::Amount::from_sat(50_000_000), + script_pubkey: coinbase_spk.clone(), + }, + block_hash: block.block_hash(), + header_code: 0, + }; + + let coinbase_out = PollardAddition:: { + hash: coinbase_out_leaf._get_leaf_hashes().into(), + remember: true, + }; + + mempool + .consume_block(&block, Proof::default(), &[coinbase_out], &[], 0, true) + .expect("failed to consume block"); + + let spending_tx = bitcoin::Transaction { + version: Version::ONE, + lock_time: absolute::LockTime::from_consensus(0), + input: vec![bitcoin::TxIn { + previous_output: OutPoint { + txid: coinbase_id, + vout: 0, + }, + script_sig: ScriptBuf::default(), + sequence: Sequence::MAX, + witness: Witness::new(), + }], + output: vec![bitcoin::TxOut { + value: bitcoin::Amount::from_sat(50_000_000), + script_pubkey: coinbase_spk.clone(), + }], + }; + + let hashes = BlockHashProvider { + block_hash: [(0, block.block_hash())].iter().cloned().collect(), + }; + + mempool + .accept_to_mempool_no_acc(spending_tx) + .expect("failed to accept to mempool"); + + let block = mempool.get_block_template( + block::Version::ONE, + block.block_hash(), + 0, + Target::MAX_ATTAINABLE_REGTEST.to_compact_lossy(), + ); + + let MempoolProof { + proof, + target_hashes, + .. + } = mempool + .get_block_proof(&block, hashes) + .expect("failed to get block proof"); + + assert!(mempool.acc.verify(&proof, &target_hashes).is_ok()); + } + #[test] fn test_random() { // just sanity check for build_transactions - let transactions = build_transactions(42, false); + let transactions = build_transactions(42, true); assert!(!transactions.is_empty()); let transactions2 = build_transactions(42, true); assert!(!transactions2.is_empty()); assert_eq!(transactions, transactions2); - + let transactions3 = build_transactions(43, true); assert!(!transactions3.is_empty()); assert_ne!(transactions, transactions3); @@ -516,12 +828,14 @@ mod tests { let len = transactions.len(); for tx in transactions { - mempool.accept_to_mempool_no_acc(tx).expect("failed to accept to mempool"); + mempool + .accept_to_mempool_no_acc(tx) + .expect("failed to accept to mempool"); } assert_eq!(mempool.transactions.len(), len); } - + #[test] fn test_gbt_with_conflict() { let mut mempool = super::Mempool::new( @@ -529,36 +843,46 @@ mod tests { 10_000_000, ); - let transactions = build_transactions(42, true); - let len = transactions.len(); - + let transactions = build_transactions(21, true); + let mut did_confict = false; for tx in transactions { - if let Err(super::AcceptToMempoolError::ConflictingTransaction) = mempool.accept_to_mempool_no_acc(tx) { + if mempool.accept_to_mempool_no_acc(tx).is_ok() { did_confict = true; } } - + // we expect at least one conflict assert!(did_confict); - + let target = Target::MAX_ATTAINABLE_REGTEST; - let mut block = mempool.get_block_template( + let block = mempool.get_block_template( block::Version::ONE, bitcoin::BlockHash::all_zeros(), 0, target.to_compact_lossy(), ); - - while !block.header.validate_pow(target).is_ok() { - block.header.nonce += 1; - } assert!(block.check_merkle_root()); - + + check_block_transactions(block); + } + + fn check_block_transactions(block: Block) { // make sure that all outputs are spent after being created, and only once let mut outputs = HashSet::new(); for tx in block.txdata.iter() { + for input in tx.input.iter() { + if input.previous_output.txid == bitcoin::Txid::all_zeros() { + continue; + } + + assert!( + outputs.remove(&input.previous_output), + "double spend {input:?}" + ); + } + for (vout, _) in tx.output.iter().enumerate() { let output = OutPoint { txid: tx.compute_txid(), @@ -567,15 +891,127 @@ mod tests { outputs.insert(output); } } + } - for tx in block.txdata.iter() { - for input in tx.input.iter() { - if input.previous_output.txid == bitcoin::Txid::all_zeros() { - continue; + #[test] + fn test_gbt_first_transaction() { + // this test will recreate the network state on block 269, and then submit the famous + // first non-coinbase transaction to the mempool, and then create a block template + // builds a proof for it, and then consumes the block. After that, we'll have a network at + // block 270, with the transaction confirmed. + + let roots = [ + "69482b799cf46ed514b01ce0573730a89c537018636b8c52a8864d5968b917f3", + "53c92fa0792c9af1c19793b1149e7fe209c69b320ea054338f53f8fd8535f2e8", + "6096c8421c1f86a9caa26e972dccdb964e280164fb060a576d51f5844e259569", + "fd46029ebb0c19e2d468a9b24d20519c64ccc342e6a32b95c86a57489b6d2504", + ] + .into_iter() + .map(|x| BitcoinNodeHash::from_str(x).unwrap()) + .collect::>(); + + let acc = Pollard::from_roots(roots, 169); + let proof_hashes = [ + "8be90393e71aa65710270b51857b538458dabd7769d801d6bbcbabe32c317251", + "5ae3964e9cc3c9e188de778c5b5fb19eaa60bce98facf1e9e68b3c1257d08c00", + "2c8dbc0642bd41cd8625344f99ef6513e5e68c03e184fcd401bddce6eba97674", + "1a55f3d560fa01fb6114842e7b4d7a0b8461f399e646f1762e6edf4be055b4dd", + "d1d2e49bce194f31dc9f3ec1cb8df3b95e097633ef42fd3723e629c9bed85ae5", + "15aba691713052033954935777d8089f4ca6b0573c7ad89fe1d0d85bbbe21846", + "8f22055465f568fd2bf9d19b285fcf2539ffea59a3cb096a3a0645366adea1b0", + ] + .into_iter() + .map(|x| BitcoinNodeHash::from_str(x).unwrap()) + .collect::>(); + + let proof = Proof::new(vec![8], proof_hashes); + let del_hashes = ["427aceafd82c11cb53a2b78f408ece6fcacf2a5b9feb5fc45cdcf36627d68d76"] + .into_iter() + .map(|x| BitcoinNodeHash::from_str(x).unwrap()) + .collect::>(); + + let prevout: LeafData = deserialize_hex("0508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d00000000c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000001300000000f2052a0100000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac").unwrap(); + + let mut mempool = Mempool::new(acc, 10_000_000); + + let tx_hex = "0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000"; + let tx: Transaction = deserialize_hex(tx_hex).unwrap(); + + let compact_leaf = CompactLeafData { + amount: prevout.utxo.value.to_sat(), + spk_ty: proof_util::get_script_type(&prevout.utxo.script_pubkey), + header_code: (9 << 1) | 1_u32, + }; + + let targets = proof.targets.clone(); + mempool + .accept_to_mempool( + tx.clone(), + proof, + &[(prevout.prevout, compact_leaf.clone())], + &del_hashes, + &targets, + ) + .expect("failed to accept to mempool"); + + let block = mempool.get_block_template( + block::Version::ONE, + bitcoin::BlockHash::from_str( + "000000002a22cfee1f2c846adbd12b3e183d4f97683f85dad08a79780a84bd55", + ) + .unwrap(), + 1231731025, + Target::MAX_ATTAINABLE_MAINNET.to_compact_lossy(), + ); + + let MempoolProof { + proof, + target_hashes, + .. + } = mempool + .get_block_proof( + &block, + BlockHashProvider { + block_hash: [( + 9, + bitcoin::BlockHash::from_str( + "000000008d9dc510f23c2657fc4f67bea30078cc05a90eb89e84cc475c080805", + ) + .unwrap(), + )] + .iter() + .cloned() + .collect(), + }, + ) + .expect("failed to get block proof"); + + let adds = tx + .output + .iter() + .enumerate() + .map(|(vout, output)| { + let leaf_data = LeafData { + prevout: OutPoint { + txid: tx.compute_txid(), + vout: vout as u32, + }, + utxo: output.clone(), + block_hash: block.block_hash(), + header_code: 170 << 1, + }; + + PollardAddition { + hash: leaf_data._get_leaf_hashes().into(), + remember: true, } - assert!(outputs.remove(&input.previous_output), "double spend {input:?}"); - } - } + }) + .collect::>(); + + assert!(mempool.acc.verify(&proof, &del_hashes).is_ok()); + mempool + .consume_block(&block, proof, &adds, &target_hashes, 170, true) + .expect("failed to consume block"); } #[test] @@ -589,44 +1025,22 @@ mod tests { let len = transactions.len(); for tx in transactions { - mempool.accept_to_mempool_no_acc(tx).expect("failed to accept to mempool"); + mempool + .accept_to_mempool_no_acc(tx) + .expect("failed to accept to mempool"); } - + let target = Target::MAX_ATTAINABLE_REGTEST; - let mut block = mempool.get_block_template( + let block = mempool.get_block_template( block::Version::ONE, bitcoin::BlockHash::all_zeros(), 0, target.to_compact_lossy(), ); - - while !block.header.validate_pow(target).is_ok() { - block.header.nonce += 1; - } - assert_eq!(block.txdata.len(), len); assert!(block.check_merkle_root()); - - // make sure that all outputs are spent after being created, and only once - let mut outputs = HashSet::new(); - for tx in block.txdata.iter() { - for (vout, _) in tx.output.iter().enumerate() { - let output = OutPoint { - txid: tx.compute_txid(), - vout: vout as u32, - }; - outputs.insert(output); - } - } - for tx in block.txdata.iter() { - for input in tx.input.iter() { - if input.previous_output.txid == bitcoin::Txid::all_zeros() { - continue; - } - assert!(outputs.remove(&input.previous_output), "double spend {input:?}"); - } - } + check_block_transactions(block); } } diff --git a/crates/floresta-wire/src/p2p_wire/running_node.rs b/crates/floresta-wire/src/p2p_wire/running_node.rs index 9e6c8ee6..d0c536b7 100644 --- a/crates/floresta-wire/src/p2p_wire/running_node.rs +++ b/crates/floresta-wire/src/p2p_wire/running_node.rs @@ -746,7 +746,7 @@ where .mempool .lock() .await - .consume_block(&block.block, proof, &adds, &del_hashes) + .consume_block(&block.block, proof, &adds, &del_hashes, block_height, false) .unwrap_or(Vec::new()); debug!(