Chain Backend API
We will now take a look at the API that the Chain
backend (from UtreexoNode
) is required to expose, as part of the BlockchainInterface
and UpdatableChainstate
traits.
The lists below are only meant to provide an initial sense of the expected chain API.
The BlockchainInterface Trait
The BlockchainInterface
methods are mainly about getting information from the current view of the blockchain and state of validation.
It has an associated generic Error
type bounded by the core2::error::Error
trait. In other words, implementations of BlockchainInterface
can choose their own error type as long as it implements core2::error::Error
.
core2
is a crate providing ano_std
version of thestd::error::Error
trait.
The list of required methods:
get_block_hash
, given a u32 height.get_tx
, given its txid.get_height
of the chain.broadcast
a transaction to the network.estimate_fee
for inclusion in usize target blocks.get_block
, given its hash.get_best_block
hash and height.get_block_header
, given its hash.is_in_ibd
, whether we are in Initial Block Download (IBD) or not.get_unbroadcasted
transactions.is_coinbase_mature
, given its block hash and height (on the mainchain, coinbase transactions mature after 100 blocks).get_block_locator
, i.e. a compact list of block hashes used to efficiently identify the most recent common point in the blockchain between two nodes for synchronization purposes.get_block_locator_for_tip
, given the hash of the tip block. This can be used for tips that are not canonical or best.get_validation_index
, i.e. the height of the last block we have validated.get_block_height
, given its block hash.get_chain_tips
block hashes, including the best tip and non-canonical ones.get_fork_point
, to get the block hash where a given branch forks (the branch is represented by its tip block hash).get_params
, to get the parameters for chain consensus.
Also, we have a subscribe
method which allows other components to receive notifications of new validated blocks from the blockchain backend.
Filename: floresta-chain/src/pruned_utreexo/mod.rs
// Path: floresta-chain/src/pruned_utreexo/mod.rs
pub trait BlockchainInterface {
type Error: core2::error::Error + Send + Sync + 'static;
// ...
fn get_block_hash(&self, height: u32) -> Result<bitcoin::BlockHash, Self::Error>;
fn get_tx(&self, txid: &bitcoin::Txid) -> Result<Option<bitcoin::Transaction>, Self::Error>;
fn get_height(&self) -> Result<u32, Self::Error>;
fn broadcast(&self, tx: &bitcoin::Transaction) -> Result<(), Self::Error>;
fn estimate_fee(&self, target: usize) -> Result<f64, Self::Error>;
fn get_block(&self, hash: &BlockHash) -> Result<Block, Self::Error>;
fn get_best_block(&self) -> Result<(u32, BlockHash), Self::Error>;
fn get_block_header(&self, hash: &BlockHash) -> Result<BlockHeader, Self::Error>;
fn subscribe(&self, tx: Arc<dyn BlockConsumer>);
// ...
fn is_in_idb(&self) -> bool;
fn get_unbroadcasted(&self) -> Vec<Transaction>;
fn is_coinbase_mature(&self, height: u32, block: BlockHash) -> Result<bool, Self::Error>;
fn get_block_locator(&self) -> Result<Vec<BlockHash>, Self::Error>;
fn get_block_locator_for_tip(&self, tip: BlockHash) -> Result<Vec<BlockHash>, BlockchainError>;
fn get_validation_index(&self) -> Result<u32, Self::Error>;
fn get_block_height(&self, hash: &BlockHash) -> Result<Option<u32>, Self::Error>;
fn update_acc(
&self,
acc: Stump,
block: UtreexoBlock,
height: u32,
proof: Proof,
del_hashes: Vec<sha256::Hash>,
) -> Result<Stump, Self::Error>;
fn get_chain_tips(&self) -> Result<Vec<BlockHash>, Self::Error>;
fn validate_block(
&self,
block: &Block,
proof: Proof,
inputs: HashMap<OutPoint, TxOut>,
del_hashes: Vec<sha256::Hash>,
acc: Stump,
) -> Result<(), Self::Error>;
fn get_fork_point(&self, block: BlockHash) -> Result<BlockHash, Self::Error>;
fn get_params(&self) -> bitcoin::params::Params;
}
// Path: floresta-chain/src/pruned_utreexo/mod.rs
pub enum Notification {
NewBlock((Block, u32)),
}
Any type that implements the BlockConsumer
trait can subscribe
to our BlockchainInterface
by passing a reference of itself, and receive notifications of new blocks (including block data and height). In the future this can be extended to also notify transactions.
Validation Methods
Finally, there are two validation methods that do NOT update the node state:
update_acc
, to get the new accumulator after applying a new block. It requires the current accumulator, the new block data, the inclusion proof for the spent UTXOs, and the hashes of the spent UTXOs.validate_block
, which instead of only verifying the inclusion proof, validates the whole block (including its transactions, for which the spent UTXOs themselves are needed).
The UpdatableChainstate Trait
On the other hand, the methods required by UpdatableChainstate
are expected to update the node state.
These methods use the BlockchainError
enum, found in pruned_utreexo/error.rs. Each variant of BlockchainError
represents a kind of error that is expected to occur (block validation errors, invalid utreexo proofs, etc.). The UpdatableChainstate
methods are:
Very important
connect_block
: Takes a block and utreexo data, validates the block and adds it to our chain.accept_header
: Checks a header and saves it in storage. This is called beforeconnect_block
, which is responsible for accepting or rejecting the actual block.
// Path: floresta-chain/src/pruned_utreexo/mod.rs
pub trait UpdatableChainstate {
fn connect_block(
&self,
block: &Block,
proof: Proof,
inputs: HashMap<OutPoint, TxOut>,
del_hashes: Vec<sha256::Hash>,
) -> Result<u32, BlockchainError>;
// ...
fn switch_chain(&self, new_tip: BlockHash) -> Result<(), BlockchainError>;
fn accept_header(&self, header: BlockHeader) -> Result<(), BlockchainError>;
// ...
fn handle_transaction(&self) -> Result<(), BlockchainError>;
fn flush(&self) -> Result<(), BlockchainError>;
fn toggle_ibd(&self, is_ibd: bool);
fn invalidate_block(&self, block: BlockHash) -> Result<(), BlockchainError>;
fn mark_block_as_valid(&self, block: BlockHash) -> Result<(), BlockchainError>;
fn get_root_hashes(&self) -> Vec<BitcoinNodeHash>;
fn get_partial_chain(
&self,
initial_height: u32,
final_height: u32,
acc: Stump,
) -> Result<PartialChainState, BlockchainError>;
fn mark_chain_as_assumed(&self, acc: Stump, tip: BlockHash) -> Result<bool, BlockchainError>;
}
Usually, in IBD we fetch a chain of headers with sufficient PoW first, and only then do we ask for the block data (i.e. the transactions) in order to verify the blocks. This way we ensure that DoS attacks sending our node invalid blocks, with the purpose of wasting our resources, are costly because of the required PoW.
Others
switch_chain
: Reorg to another branch, given its tip block hash.handle_transaction
: Process transactions that are in the mempool.flush
: Writes pending data to storage. Should be invoked periodically.toggle_ibd
: Toggle the IBD process on/off.invalidate_block
: Tells the blockchain backend to consider this block invalid.mark_block_as_valid
: Overrides a block that was marked as invalid, considering it as fully validated.get_root_hashes
: Returns the root hashes of our utreexo accumulator.get_partial_chain
: Returns aPartialChainState
(a Floresta type allowing to validate parts of the chain in parallel, explained in Chapter 5), given the height range and the initial utreexo state.mark_chain_as_assumed
: Given a block hash and the corresponding accumulator, assume every ancestor block is valid.