From 9b463c4ac7b91eb46f54cddfbda01aad7fc424e0 Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Fri, 26 Jan 2024 14:03:10 -0300 Subject: [PATCH] create partial chainstates from a full chainstate --- .../src/pruned_utreexo/chain_state.rs | 30 ++ .../src/pruned_utreexo/error.rs | 5 + .../floresta-chain/src/pruned_utreexo/mod.rs | 19 + .../src/pruned_utreexo/partial_chain.rs | 345 +++++++++++++++--- .../src/pruned_utreexo/udata.rs | 68 +++- crates/floresta-common/src/lib.rs | 3 +- crates/floresta-wire/src/p2p_wire/node.rs | 3 +- 7 files changed, 429 insertions(+), 44 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index 6df825f8..bc4e7f5f 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -7,6 +7,7 @@ use alloc::fmt::format; use alloc::string::ToString; use alloc::sync::Arc; use alloc::vec::Vec; +use core::cell::UnsafeCell; #[cfg(feature = "bitcoinconsensus")] use core::ffi::c_uint; @@ -41,6 +42,8 @@ use super::chainstore::KvChainStore; use super::consensus::Consensus; use super::error::BlockValidationErrors; use super::error::BlockchainError; +use super::partial_chain::PartialChainState; +use super::partial_chain::PartialChainStateInner; use super::BlockchainInterface; use super::ChainStore; use super::UpdatableChainstate; @@ -107,6 +110,7 @@ impl ChainState { _ => {} } } + /// Just adds headers to the chainstate, without validating them. pub fn push_headers( &self, @@ -1033,6 +1037,32 @@ impl UpdatableChainstate for ChainState Result { + let blocks = (initial_height..=final_height) + .map(|height| self.get_block_header_by_height(height)) + .collect(); + + let inner = PartialChainStateInner { + error: None, + blocks, + consensus: Consensus { + parameters: self.chain_params(), + }, + current_acc: acc, + final_height, + assume_valid: false, + initial_height, + current_height: initial_height, + }; + + Ok(PartialChainState(UnsafeCell::new(inner))) + } } impl From> for ChainState { diff --git a/crates/floresta-chain/src/pruned_utreexo/error.rs b/crates/floresta-chain/src/pruned_utreexo/error.rs index 37c95012..93f143e6 100644 --- a/crates/floresta-chain/src/pruned_utreexo/error.rs +++ b/crates/floresta-chain/src/pruned_utreexo/error.rs @@ -38,7 +38,9 @@ pub enum BlockValidationErrors { BlockExtendsAnOrphanChain, BadBip34, InvalidProof, + CoinbaseNotMatured, } + impl Display for BlockValidationErrors { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -67,6 +69,9 @@ impl Display for BlockValidationErrors { } BlockValidationErrors::BadBip34 => write!(f, "BIP34 commitment mismatch"), BlockValidationErrors::InvalidProof => write!(f, "Invalid proof"), + BlockValidationErrors::CoinbaseNotMatured => { + write!(f, "Coinbase not matured yet") + } } } } diff --git a/crates/floresta-chain/src/pruned_utreexo/mod.rs b/crates/floresta-chain/src/pruned_utreexo/mod.rs index 713a1167..fce07982 100644 --- a/crates/floresta-chain/src/pruned_utreexo/mod.rs +++ b/crates/floresta-chain/src/pruned_utreexo/mod.rs @@ -20,7 +20,9 @@ use bitcoin::Transaction; use bitcoin::TxOut; use rustreexo::accumulator::node_hash::NodeHash; use rustreexo::accumulator::proof::Proof; +use rustreexo::accumulator::stump::Stump; +use self::partial_chain::PartialChainState; use crate::prelude::*; use crate::BestChain; use crate::BlockConsumer; @@ -102,6 +104,23 @@ pub trait UpdatableChainstate { fn process_rescan_block(&self, block: &Block) -> Result<(), BlockchainError>; /// Returns the root hashes of our utreexo forest fn get_root_hashes(&self) -> Vec; + /// Returns a partial chainstate from a range of blocks. + /// + /// [PartialChainState] is a simplified version of `ChainState` that is used during IBD. + /// It doesn't suport reorgs, only hold headers for a subset of blocks and isn't [Sync]. + /// The idea here is that you take a OS thread or some async task that will drive one + /// [PartialChainState] to completion by downloading blocks inside that chainstate's range. + /// If all goes right, it'll end without error, and you should mark blocks in this range as + /// valid. + /// + /// Since this chainstate may start from a height with an existing UTXO set, you need to + /// provide a [Stump] for that block. + fn get_partial_chain( + &self, + initial_height: u32, + final_height: u32, + acc: Stump, + ) -> Result; } /// [ChainStore] is a trait defining how we interact with our chain database. This definitions diff --git a/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs b/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs index 07ec8242..c1c2865b 100644 --- a/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs +++ b/crates/floresta-chain/src/pruned_utreexo/partial_chain.rs @@ -1,10 +1,30 @@ //! A partial chain is a chain that only contains a subset of the blocks in the //! full chain. We use multiple partial chains to sync up with the full chain, -//! and then merge them together to get the full chain. This allows us to conduct -//! the sync in parallel. - +//! and then merge them together to get the full chain. This allows us to make +//! Initial Block Download in parallel. +//! +//! We use a [PartialChainState] insted of the useal ChainState, mainly for +//! performance. Because we assume that only one worker will hold a [PartialChainState] +//! at a given time, we can drop all syncronization primitives and make a really performatic +//! ChainState that will consume and validate blocks as fast as we possibly can. +//! +//! This choice removes the use of costly atomic operations, but opens space for design flaws +//! and memory unsoundness, so here are some tips about this module and how people looking for +//! extend or use this code should proceed: +//! +//! - Shared ownership is forbidden: if you have two threads or tasks owning this, you'll have +//! data race. If you want to hold shared ownership for this module, you need to place a +//! [PartialChainState] inside an `Arc` yourself. Note that you can't just Arc this, +//! because [PartialChainState] isn't [Sync]. +//! - The interior is toxic, so no peeking: no references, mutable or not, to any field should +//! leak through the API, as we are not enforcing lifetime or borrowing rules at compile time. +//! - Sending is fine: There's nothing in this module that makes it not sendable to between +//! threads, as long as the origin thread gives away the ownership. +use bitcoin::BlockHash; use floresta_common::prelude::*; extern crate alloc; + +use core::cell::UnsafeCell; #[cfg(feature = "bitcoinconsensus")] use core::ffi::c_uint; @@ -18,65 +38,99 @@ use super::chainparams::ChainParams; use super::consensus::Consensus; use super::error::BlockValidationErrors; use super::error::BlockchainError; +use super::BlockchainInterface; +use super::UpdatableChainstate; -/// A partial chain is a chain that only contains a subset of the blocks in the -/// full chain. We use multiple partial chains to sync up with the full chain, -/// and then merge them together to get the full chain. This allows us to conduct -/// the sync in parallel. To build one, we need to know the initial -/// height, the final height, and the block headers in between. -pub struct PartialChainState { +#[doc(hidden)] +#[derive(Debug)] +pub(crate) struct PartialChainStateInner { /// The current accumulator state, it starts with a hardcoded value and /// gets checked against the result of the previous partial chainstate. - current_acc: Stump, + pub(crate) current_acc: Stump, /// The block headers in this interval, we need this to verify the blocks /// and to build the accumulator. We assume this is sorted by height, and /// should contains all blocks in this interval. - blocks: Vec, + pub(crate) blocks: Vec, /// The height this interval starts at. This [initial_height, final_height), so /// if we break the interval at height 100, the first interval will be [0, 100) /// and the second interval will be [100, 200). And the initial height of the /// second interval will be 99. - initial_height: u32, + pub(crate) initial_height: u32, /// The height we are on right now, this is used to keep track of the progress /// of the sync. - current_height: u32, + pub(crate) current_height: u32, /// The height we are syncing up to, trying to push more blocks than this will /// result in an error. - final_height: u32, + pub(crate) final_height: u32, /// The error that occurred during validation, if any. It is here so we can /// pull that afterwords. - error: Option, + pub(crate) error: Option, /// The consensus parameters, we need this to validate the blocks. - consensus: Consensus, + pub(crate) consensus: Consensus, /// Whether we assume the signatures in this interval as valid, this is used to /// speed up syncing, by assuming signatures in old blocks are valid. - assume_valid: bool, + pub(crate) assume_valid: bool, } -impl PartialChainState { + +/// A partial chain is a chain that only contains a subset of the blocks in the +/// full chain. We use multiple partial chains to sync up with the full chain, +/// and then merge them together to get the full chain. This allows us to conduct +/// the sync in parallel. To build one, we need to know the initial +/// height, the final height, and the block headers in between. +/// +/// We need to modify our current state as-we-go, but we also need to use the main +/// traits that define a chainstate. Most cruccially, both crates don't take a mutable +/// reference in any method, so we need some form of interior mutability. +/// We could just use a mutex, but this is not required and very wateful. Partial chains +/// differ from the normal chain because they only have one owner, the worker responsible +/// for driving this chain to it's completion. Because of that, we can simply use a UnsafeCell +/// and forbit shared access between threads (i.e. don't implement [Sync]) +pub struct PartialChainState(pub(crate) UnsafeCell); + +/// We need to send [PartialChainState] between threads/tasks, because the worker thread, once it +/// finishes, needs to notify the main task and pass the final partial chain. +/// # Safety +/// +/// All itens inside the [UnsafeCell] are [Send], most importantly, there are no references or +/// smart pointers inside it, so sending shouldn't be a problem. +/// +/// Note that [PartialChainState] isn't meant to be shared among threads, so we shouldn't implement [Sync]. +/// The idea of a partial chain is be owned by a single worker that will download all blocks in +/// the range covered by this chain and validate each block, so only the worker thread (or task) +/// will own this at any given time. +unsafe impl Send for PartialChainState {} + +impl PartialChainStateInner { /// Returns the height we started syncing from pub fn initial_height(&self) -> u32 { self.initial_height } + /// Is this interval valid? pub fn is_valid(&self) -> bool { self.is_sync() && self.error.is_none() } + /// Returns the validation error, if any pub fn error(&self) -> Option { self.error.clone() } + /// Returns the height we have synced up to so far pub fn current_height(&self) -> u32 { self.current_height } + /// Whether or not we have synced up to the final height pub fn is_sync(&self) -> bool { self.current_height == self.final_height } + pub fn get_block(&self, height: u32) -> Option<&BlockHeader> { let index = height - self.initial_height; self.blocks.get(index as usize) } + #[cfg(feature = "bitcoinconsensus")] /// Returns the validation flags, given the current block height fn get_validation_flags(&self, height: u32) -> c_uint { @@ -117,17 +171,20 @@ impl PartialChainState { self.current_height = height; self.current_acc = acc; } + #[inline] /// Returns the parameters for this chain fn chain_params(&self) -> ChainParams { self.consensus.parameters.clone() } + #[inline] /// Returns the ancestor for a given block header fn get_ancestor(&self, height: u32) -> Result { let prev = self.get_block(height - 1).unwrap(); Ok(*prev) } + /// Process a block, given the proof, inputs, and deleted hashes. If we find an error, /// we save it. pub fn process_block( @@ -136,18 +193,20 @@ impl PartialChainState { proof: rustreexo::accumulator::proof::Proof, inputs: HashMap, del_hashes: Vec, - ) -> bool { + ) -> Result { let height = self.current_height + 1; + if let Err(BlockchainError::BlockValidation(e)) = self.validate_block(block, height, inputs) { - self.error = Some(e); - return false; + self.error = Some(e.clone()); + return Err(BlockchainError::BlockValidation(e)); } + let acc = match Consensus::update_acc(&self.current_acc, block, height, proof, del_hashes) { Ok(acc) => acc, Err(_) => { self.error = Some(BlockValidationErrors::InvalidProof); - return false; + return Err(BlockchainError::InvalidProof); } }; @@ -161,12 +220,14 @@ impl PartialChainState { } self.update_state(height, acc); - true + Ok(height) } + /// Is the current accumulator what we expect? pub fn is_expected(&self, acc: Stump) -> bool { self.current_acc == acc } + /// Check whether a block is valid fn validate_block( &self, @@ -220,8 +281,193 @@ impl PartialChainState { } } +impl PartialChainState { + /// Borrows the inner content as immutable referece. + /// + /// # Safety + /// We can assume this [UnsafeCell] is initialized because the only way to get a + /// [PartialChainState] is through our APIs, and we make sure this [UnsafeCell] is + /// always valid. + /// The reference returned here **should not** leak through the API, as there's no + /// syncronization mechanims for it. + #[inline(always)] + #[must_use] + #[doc(hidden)] + fn inner(&self) -> &PartialChainStateInner { + unsafe { self.0.get().as_ref().expect("this pointer is valid") } + } + + /// Borrows the inner content as a mutable referece. + /// + /// # Safety + /// We can assume this [UnsafeCell] is initialized because the only way to get a + /// [PartialChainState] is through our APIs, and we make sure this [UnsafeCell] is + /// always valid. + /// The reference returned here **should not** leak through the API, as there's no + /// syncronization mechanims for it. + #[inline(always)] + #[allow(clippy::mut_from_ref)] + #[must_use] + #[doc(hidden)] + fn inner_mut(&self) -> &mut PartialChainStateInner { + unsafe { self.0.get().as_mut().expect("this pointer is valid") } + } +} + +impl UpdatableChainstate for PartialChainState { + fn connect_block( + &self, + block: &bitcoin::Block, + proof: rustreexo::accumulator::proof::Proof, + inputs: HashMap, + del_hashes: Vec, + ) -> Result { + self.inner_mut() + .process_block(block, proof, inputs, del_hashes) + } + + fn get_root_hashes(&self) -> Vec { + self.inner().current_acc.roots.clone() + } + + //these are no-ops, you can call them, but they won't do anything + + fn flush(&self) -> Result<(), BlockchainError> { + // no-op: we keep everything on memory + Ok(()) + } + + fn toggle_ibd(&self, _is_ibd: bool) { + // no-op: we know if we finished by looking at our current and end height + } + + // these are unimplemented, and will panic if called + + fn accept_header(&self, _header: BlockHeader) -> Result<(), BlockchainError> { + unimplemented!("partialChainState shouldn't be used to accept new headers") + } + + fn get_partial_chain( + &self, + _initial_height: u32, + _final_height: u32, + _acc: Stump, + ) -> Result { + unimplemented!("We are a partial chain") + } + + fn invalidate_block(&self, _block: BlockHash) -> Result<(), BlockchainError> { + unimplemented!("we know if a block is invalid, just break out of your loop and use the is_valid() method") + } + + fn handle_transaction(&self) -> Result<(), BlockchainError> { + unimplemented!("we don't do transactions") + } + + fn process_rescan_block(&self, _block: &bitcoin::Block) -> Result<(), BlockchainError> { + unimplemented!("we don't do rescan") + } +} + +impl BlockchainInterface for PartialChainState { + type Error = BlockchainError; + + fn get_height(&self) -> Result { + Ok(self.inner().current_height) + } + + fn get_block_hash(&self, height: u32) -> Result { + let height = height - self.inner().initial_height; + self.inner() + .blocks + .get(height as usize) + .map(|b| b.block_hash()) + .ok_or(BlockchainError::BlockNotPresent) + } + + fn get_best_block(&self) -> Result<(u32, bitcoin::BlockHash), Self::Error> { + Ok(( + self.inner().current_height(), + self.get_block_hash(self.inner().current_height())?, + )) + } + + fn is_coinbase_mature( + &self, + height: u32, + _block: bitcoin::BlockHash, + ) -> Result { + let current_height = self.inner().current_height; + let coinbase_maturity = self.inner().chain_params().coinbase_maturity; + + Ok(height + coinbase_maturity > current_height) + } + + fn is_in_idb(&self) -> bool { + !self.inner().is_sync() + } + + // partial chain states are only used for IBD, so we don't need to implement these + + fn get_block_header(&self, _height: &BlockHash) -> Result { + unimplemented!("PartialChainState::get_block_header") + } + + fn get_block(&self, _hash: &bitcoin::BlockHash) -> Result { + unimplemented!("PartialChainState::get_block") + } + + fn get_tx(&self, _txid: &bitcoin::Txid) -> Result, Self::Error> { + unimplemented!("partialChainState::get_tx") + } + + fn rescan(&self, _start_height: u32) -> Result<(), Self::Error> { + unimplemented!("partialChainState::rescan") + } + + fn broadcast(&self, _tx: &bitcoin::Transaction) -> Result<(), Self::Error> { + unimplemented!("partialChainState::broadcast") + } + + fn subscribe(&self, _tx: sync::Arc) { + unimplemented!("partialChainState::subscibe") + } + + fn estimate_fee(&self, _target: usize) -> Result { + unimplemented!("partialChainState::estimate_fee") + } + + fn get_rescan_index(&self) -> Option { + unimplemented!("partialChainState::get_rescan_index") + } + + fn get_block_height(&self, _hash: &bitcoin::BlockHash) -> Result, Self::Error> { + unimplemented!("partialChainState::get_block_height") + } + + fn get_unbroadcasted(&self) -> Vec { + unimplemented!("partialChainState::get_unbroadcasted") + } + + fn get_block_locator(&self) -> Result, Self::Error> { + unimplemented!("partialChainState::get_block_locator") + } + + fn get_validation_index(&self) -> Result { + unimplemented!("partialChainState::get_validation_index") + } +} + +// mainly for tests +impl From for PartialChainState { + fn from(value: PartialChainStateInner) -> Self { + PartialChainState(UnsafeCell::new(value)) + } +} + #[cfg(test)] mod tests { + use core::str::FromStr; use std::collections::HashMap; @@ -236,18 +482,24 @@ mod tests { use crate::pruned_utreexo::chainparams::ChainParams; use crate::pruned_utreexo::consensus::Consensus; use crate::pruned_utreexo::error::BlockValidationErrors; + use crate::pruned_utreexo::partial_chain::PartialChainStateInner; + use crate::pruned_utreexo::UpdatableChainstate; + use crate::BlockchainError; use crate::Network; + #[test] fn test_with_invalid_block() { fn run(block: &str, reason: BlockValidationErrors) { let genesis = parse_block("0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f20020000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"); let block = parse_block(block); - let mut chainstate = get_empty_pchain(vec![genesis.header, block.header]); + let chainstate = get_empty_pchain(vec![genesis.header, block.header]); + let res = chainstate.connect_block(&block, Proof::default(), HashMap::new(), vec![]); - assert!(!chainstate.process_block(&block, Proof::default(), HashMap::new(), vec![])); - assert!(!chainstate.is_valid()); - assert_eq!(chainstate.error, Some(reason)); + match res { + Err(BlockchainError::BlockValidation(_e)) if matches!(reason, _e) => {} + _ => panic!("unexpected {res:?}"), + }; } run("0000002000226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f39adbcd7823048d34357bdca86cd47172afe2a4af8366b5b34db36df89386d49b23ec964ffff7f20000000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff165108feddb99c6b8435060b2f503253482f627463642fffffffff0100f2052a01000000160014806cef41295922d32ddfca09c26cc4acd36c3ed000000000",super::BlockValidationErrors::BlockExtendsAnOrphanChain); run("0000002000226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f40adbcd7823048d34357bdca86cd47172afe2a4af8366b5b34db36df89386d49b23ec964ffff7f20000000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff165108feddb99c6b8435060b2f503253482f627463642fffffffff0100f2052a01000000160014806cef41295922d32ddfca09c26cc4acd36c3ed000000000", BlockValidationErrors::BadMerkleRoot); @@ -257,7 +509,7 @@ mod tests { deserialize(&block).unwrap() } fn get_empty_pchain(blocks: Vec
) -> PartialChainState { - PartialChainState { + PartialChainStateInner { assume_valid: true, consensus: Consensus { parameters: ChainParams::from(Network::Regtest), @@ -269,7 +521,9 @@ mod tests { error: None, initial_height: 0, } + .into() } + #[test] fn test_updating_single_chain() { let blocks = include_str!("./testdata/blocks.txt"); @@ -281,7 +535,7 @@ mod tests { let block: Block = deserialize(&hex::decode(block).unwrap()).unwrap(); parsed_blocks.push(block); } - let mut chainstate = PartialChainState { + let chainstate: PartialChainState = PartialChainStateInner { assume_valid: true, consensus: Consensus { parameters: ChainParams::from(Network::Regtest), @@ -292,17 +546,21 @@ mod tests { blocks: parsed_blocks.iter().map(|block| block.header).collect(), error: None, initial_height: 0, - }; + } + .into(); parsed_blocks.remove(0); for block in parsed_blocks { let proof = Proof::default(); let inputs = HashMap::new(); let del_hashes = vec![]; - chainstate.process_block(&block, proof, inputs, del_hashes); + chainstate + .connect_block(&block, proof, inputs, del_hashes) + .unwrap(); } - assert_eq!(chainstate.current_height, 100); - assert!(chainstate.is_valid()); + assert_eq!(chainstate.inner().current_height, 100); + assert!(chainstate.inner().is_valid()); } + #[test] fn test_updating_multiple_chains() { // We have two chains, one with 100 blocks, one with 50 blocks. We expect the @@ -315,7 +573,7 @@ mod tests { } // The file contains 150 blocks, we split them into two chains. let (blocks1, blocks2) = parsed_blocks.split_at(101); - let mut chainstate1 = PartialChainState { + let mut chainstate1 = PartialChainStateInner { assume_valid: true, consensus: Consensus { parameters: ChainParams::from(Network::Regtest), @@ -341,7 +599,9 @@ mod tests { let proof = Proof::default(); let inputs = HashMap::new(); let del_hashes = vec![]; - chainstate1.process_block(block, proof, inputs, del_hashes); + chainstate1 + .process_block(block, proof, inputs, del_hashes) + .unwrap(); } // The state after 100 blocks, computed ahead of time. let roots = [ @@ -361,7 +621,7 @@ mod tests { // the hard-coded values. assert_eq!(chainstate1.current_acc, acc2); - let mut chainstate2 = PartialChainState { + let chainstate2: PartialChainState = PartialChainStateInner { assume_valid: true, consensus: Consensus { parameters: ChainParams::from(Network::Regtest), @@ -372,13 +632,16 @@ mod tests { blocks: blocks2_headers, error: None, initial_height: 100, - }; + } + .into(); for block in blocks2 { let proof = Proof::default(); let inputs = HashMap::new(); let del_hashes = vec![]; - chainstate2.process_block(block, proof, inputs, del_hashes); + chainstate2 + .connect_block(block, proof, inputs, del_hashes) + .unwrap(); } let roots = [ @@ -393,9 +656,9 @@ mod tests { let expected_acc: Stump = Stump { leaves: 150, roots }; - assert_eq!(chainstate2.current_height, 150); - assert_eq!(chainstate2.current_acc, expected_acc); + assert_eq!(chainstate2.inner().current_height, 150); + assert_eq!(chainstate2.inner().current_acc, expected_acc); - assert!(chainstate2.is_valid()); + assert!(chainstate2.inner().is_valid()); } } diff --git a/crates/floresta-chain/src/pruned_utreexo/udata.rs b/crates/floresta-chain/src/pruned_utreexo/udata.rs index 5db9a60e..e9f83d4c 100644 --- a/crates/floresta-chain/src/pruned_utreexo/udata.rs +++ b/crates/floresta-chain/src/pruned_utreexo/udata.rs @@ -13,6 +13,7 @@ use sha2::Sha512_256; use crate::prelude::*; use crate::pruned_utreexo::consensus::UTREEXO_TAG_V1; + /// Leaf data is the data that is hashed when adding to utreexo state. It contains validation /// data and some commitments to make it harder to attack an utreexo-only node. #[derive(Debug, PartialEq)] @@ -35,6 +36,7 @@ pub struct LeafData { /// The actual utxo pub utxo: TxOut, } + impl LeafData { pub fn _get_leaf_hashes(&self) -> sha256::Hash { let mut ser_utxo = Vec::new(); @@ -52,6 +54,7 @@ impl LeafData { .expect("parent_hash: Engines shouldn't be Err") } } + impl Decodable for LeafData { fn consensus_decode( reader: &mut R, @@ -73,23 +76,35 @@ impl Decodable for LeafData { }) } } + pub mod proof_util { use bitcoin::blockdata::script::Instruction; + use bitcoin::hashes::sha256; use bitcoin::hashes::Hash; use bitcoin::p2p::utreexo::CompactLeafData; + use bitcoin::p2p::utreexo::UData; use bitcoin::Amount; + use bitcoin::OutPoint; use bitcoin::PubkeyHash; use bitcoin::ScriptBuf; use bitcoin::ScriptHash; + use bitcoin::Transaction; use bitcoin::TxIn; use bitcoin::TxOut; use bitcoin::WPubkeyHash; use bitcoin::WScriptHash; + use rustreexo::accumulator::node_hash::NodeHash; + use rustreexo::accumulator::proof::Proof; + + use super::LeafData; + use crate::prelude::*; + use crate::pruned_utreexo::BlockchainInterface; + #[derive(Debug)] pub enum Error { EmptyStack, } - use super::LeafData; + pub fn reconstruct_leaf_data( leaf: &CompactLeafData, input: &TxIn, @@ -107,6 +122,57 @@ pub mod proof_util { }, }) } + + #[allow(clippy::type_complexity)] + pub fn process_proof( + udata: &UData, + transactions: &[Transaction], + chain: &Chain, + ) -> Result<(Proof, Vec, HashMap), Chain::Error> { + let targets = udata.proof.targets.iter().map(|target| target.0).collect(); + let hashes = udata + .proof + .hashes + .iter() + .map(|hash| NodeHash::Some(*hash.as_byte_array())) + .collect(); + let proof = Proof::new(targets, hashes); + let mut hashes = Vec::new(); + let mut leaves_iter = udata.leaves.iter().cloned(); + let mut tx_iter = transactions.iter(); + + let mut inputs = HashMap::new(); + tx_iter.next(); // Skip coinbase + + for tx in tx_iter { + let txid = tx.txid(); + for (vout, out) in tx.output.iter().enumerate() { + inputs.insert( + OutPoint { + txid, + vout: vout as u32, + }, + out.clone(), + ); + } + + for input in tx.input.iter() { + if !inputs.contains_key(&input.previous_output) { + if let Some(leaf) = leaves_iter.next() { + let height = leaf.header_code >> 1; + let hash = chain.get_block_hash(height)?; + let leaf = + reconstruct_leaf_data(&leaf, input, hash).expect("Invalid proof"); + hashes.push(leaf._get_leaf_hashes()); + inputs.insert(leaf.prevout, leaf.utxo); + } + } + } + } + + Ok((proof, hashes, inputs)) + } + fn reconstruct_script_pubkey(leaf: &CompactLeafData, input: &TxIn) -> Result { match &leaf.spk_ty { bitcoin::p2p::utreexo::ScriptPubkeyType::Other(spk) => { diff --git a/crates/floresta-common/src/lib.rs b/crates/floresta-common/src/lib.rs index 2e475ee4..77e3d3eb 100644 --- a/crates/floresta-common/src/lib.rs +++ b/crates/floresta-common/src/lib.rs @@ -6,11 +6,11 @@ use bitcoin::ScriptBuf; use miniscript::Descriptor; #[cfg(feature = "descriptors")] use miniscript::DescriptorPublicKey; -use prelude::*; use sha2::Digest; pub mod constants; pub mod spsc; +use prelude::*; pub use spsc::Channel; pub fn get_hash_from_u8(data: &[u8]) -> sha256::Hash { @@ -74,6 +74,7 @@ pub mod prelude { } #[cfg(not(feature = "no-std"))] pub mod prelude { + extern crate alloc; extern crate std; pub use std::borrow::ToOwned; pub use std::boxed::Box; diff --git a/crates/floresta-wire/src/p2p_wire/node.rs b/crates/floresta-wire/src/p2p_wire/node.rs index 558f464d..e1e50a95 100644 --- a/crates/floresta-wire/src/p2p_wire/node.rs +++ b/crates/floresta-wire/src/p2p_wire/node.rs @@ -1477,7 +1477,8 @@ where | BlockValidationErrors::BadCoinbaseOutValue | BlockValidationErrors::EmptyBlock | BlockValidationErrors::BlockExtendsAnOrphanChain - | BlockValidationErrors::BadBip34 => { + | BlockValidationErrors::BadBip34 + | BlockValidationErrors::CoinbaseNotMatured => { self.send_to_peer(peer, NodeRequest::Shutdown).await?; try_and_log!(self.chain.invalidate_block(block.block.block_hash())); }