use std::{
collections::{HashMap, HashSet},
ops::{Add, Deref, DerefMut, RangeInclusive},
sync::Arc,
};
use zebra_chain::{
amount::{Amount, NegativeAllowed, NonNegative},
block::{self, Block, HeightDiff},
history_tree::HistoryTree,
orchard,
parallel::tree::NoteCommitmentTrees,
sapling,
serialization::SerializationError,
sprout,
subtree::{NoteCommitmentSubtree, NoteCommitmentSubtreeIndex},
transaction::{self, UnminedTx},
transparent::{self, utxos_from_ordered_utxos},
value_balance::{ValueBalance, ValueBalanceError},
};
#[allow(unused_imports)]
use crate::{
constants::{MAX_FIND_BLOCK_HASHES_RESULTS, MAX_FIND_BLOCK_HEADERS_RESULTS},
ReadResponse, Response,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg(feature = "indexer")]
pub enum Spend {
OutPoint(transparent::OutPoint),
Sprout(sprout::Nullifier),
Sapling(sapling::Nullifier),
Orchard(orchard::Nullifier),
}
#[cfg(feature = "indexer")]
impl From<transparent::OutPoint> for Spend {
fn from(outpoint: transparent::OutPoint) -> Self {
Self::OutPoint(outpoint)
}
}
#[cfg(feature = "indexer")]
impl From<sprout::Nullifier> for Spend {
fn from(sprout_nullifier: sprout::Nullifier) -> Self {
Self::Sprout(sprout_nullifier)
}
}
#[cfg(feature = "indexer")]
impl From<sapling::Nullifier> for Spend {
fn from(sapling_nullifier: sapling::Nullifier) -> Self {
Self::Sapling(sapling_nullifier)
}
}
#[cfg(feature = "indexer")]
impl From<orchard::Nullifier> for Spend {
fn from(orchard_nullifier: orchard::Nullifier) -> Self {
Self::Orchard(orchard_nullifier)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum HashOrHeight {
Hash(block::Hash),
Height(block::Height),
}
impl HashOrHeight {
pub fn height_or_else<F>(self, op: F) -> Option<block::Height>
where
F: FnOnce(block::Hash) -> Option<block::Height>,
{
match self {
HashOrHeight::Hash(hash) => op(hash),
HashOrHeight::Height(height) => Some(height),
}
}
pub fn hash_or_else<F>(self, op: F) -> Option<block::Hash>
where
F: FnOnce(block::Height) -> Option<block::Hash>,
{
match self {
HashOrHeight::Hash(hash) => Some(hash),
HashOrHeight::Height(height) => op(height),
}
}
pub fn hash(&self) -> Option<block::Hash> {
if let HashOrHeight::Hash(hash) = self {
Some(*hash)
} else {
None
}
}
pub fn height(&self) -> Option<block::Height> {
if let HashOrHeight::Height(height) = self {
Some(*height)
} else {
None
}
}
pub fn new(hash_or_height: &str, tip_height: Option<block::Height>) -> Result<Self, String> {
hash_or_height
.parse()
.map(Self::Hash)
.or_else(|_| hash_or_height.parse().map(Self::Height))
.or_else(|_| {
hash_or_height
.parse()
.map_err(|_| "could not parse negative height")
.and_then(|d: HeightDiff| {
if d.is_negative() {
{
Ok(HashOrHeight::Height(
tip_height
.ok_or("missing tip height")?
.add(d)
.ok_or("underflow when adding negative height to tip")?
.next()
.map_err(|_| "height -1 needs to point to tip")?,
))
}
} else {
Err("height was not negative")
}
})
})
.map_err(|_| {
"parse error: could not convert the input string to a hash or height".to_string()
})
}
}
impl std::fmt::Display for HashOrHeight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HashOrHeight::Hash(hash) => write!(f, "{hash}"),
HashOrHeight::Height(height) => write!(f, "{}", height.0),
}
}
}
impl From<block::Hash> for HashOrHeight {
fn from(hash: block::Hash) -> Self {
Self::Hash(hash)
}
}
impl From<block::Height> for HashOrHeight {
fn from(height: block::Height) -> Self {
Self::Height(height)
}
}
impl From<(block::Height, block::Hash)> for HashOrHeight {
fn from((_height, hash): (block::Height, block::Hash)) -> Self {
hash.into()
}
}
impl From<(block::Hash, block::Height)> for HashOrHeight {
fn from((hash, _height): (block::Hash, block::Height)) -> Self {
hash.into()
}
}
impl std::str::FromStr for HashOrHeight {
type Err = SerializationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse()
.map(Self::Hash)
.or_else(|_| s.parse().map(Self::Height))
.map_err(|_| {
SerializationError::Parse("could not convert the input string to a hash or height")
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SemanticallyVerifiedBlock {
pub block: Arc<Block>,
pub hash: block::Hash,
pub height: block::Height,
pub new_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
pub transaction_hashes: Arc<[transaction::Hash]>,
pub deferred_balance: Option<Amount<NonNegative>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckpointVerifiedBlock(pub(crate) SemanticallyVerifiedBlock);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextuallyVerifiedBlock {
pub(crate) block: Arc<Block>,
pub(crate) hash: block::Hash,
pub(crate) height: block::Height,
pub(crate) new_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
pub(crate) spent_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
pub(crate) transaction_hashes: Arc<[transaction::Hash]>,
pub(crate) chain_value_pool_change: ValueBalance<NegativeAllowed>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Treestate {
pub note_commitment_trees: NoteCommitmentTrees,
pub history_tree: Arc<HistoryTree>,
}
impl Treestate {
pub fn new(
sprout: Arc<sprout::tree::NoteCommitmentTree>,
sapling: Arc<sapling::tree::NoteCommitmentTree>,
orchard: Arc<orchard::tree::NoteCommitmentTree>,
sapling_subtree: Option<NoteCommitmentSubtree<sapling::tree::Node>>,
orchard_subtree: Option<NoteCommitmentSubtree<orchard::tree::Node>>,
history_tree: Arc<HistoryTree>,
) -> Self {
Self {
note_commitment_trees: NoteCommitmentTrees {
sprout,
sapling,
sapling_subtree,
orchard,
orchard_subtree,
},
history_tree,
}
}
}
pub enum FinalizableBlock {
Checkpoint {
checkpoint_verified: CheckpointVerifiedBlock,
},
Contextual {
contextually_verified: ContextuallyVerifiedBlock,
treestate: Treestate,
},
}
pub struct FinalizedBlock {
pub(super) block: Arc<Block>,
pub(super) hash: block::Hash,
pub(super) height: block::Height,
pub(super) new_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
pub(super) transaction_hashes: Arc<[transaction::Hash]>,
pub(super) treestate: Treestate,
pub(super) deferred_balance: Option<Amount<NonNegative>>,
}
impl FinalizedBlock {
pub fn from_checkpoint_verified(block: CheckpointVerifiedBlock, treestate: Treestate) -> Self {
Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate)
}
pub fn from_contextually_verified(
block: ContextuallyVerifiedBlock,
treestate: Treestate,
) -> Self {
Self::from_semantically_verified(SemanticallyVerifiedBlock::from(block), treestate)
}
fn from_semantically_verified(block: SemanticallyVerifiedBlock, treestate: Treestate) -> Self {
Self {
block: block.block,
hash: block.hash,
height: block.height,
new_outputs: block.new_outputs,
transaction_hashes: block.transaction_hashes,
treestate,
deferred_balance: block.deferred_balance,
}
}
}
impl FinalizableBlock {
pub fn new(contextually_verified: ContextuallyVerifiedBlock, treestate: Treestate) -> Self {
Self::Contextual {
contextually_verified,
treestate,
}
}
#[cfg(test)]
pub fn inner_block(&self) -> Arc<Block> {
match self {
FinalizableBlock::Checkpoint {
checkpoint_verified,
} => checkpoint_verified.block.clone(),
FinalizableBlock::Contextual {
contextually_verified,
..
} => contextually_verified.block.clone(),
}
}
}
impl From<CheckpointVerifiedBlock> for FinalizableBlock {
fn from(checkpoint_verified: CheckpointVerifiedBlock) -> Self {
Self::Checkpoint {
checkpoint_verified,
}
}
}
impl From<Arc<Block>> for FinalizableBlock {
fn from(block: Arc<Block>) -> Self {
Self::from(CheckpointVerifiedBlock::from(block))
}
}
impl From<&SemanticallyVerifiedBlock> for SemanticallyVerifiedBlock {
fn from(semantically_verified: &SemanticallyVerifiedBlock) -> Self {
semantically_verified.clone()
}
}
impl ContextuallyVerifiedBlock {
pub fn with_block_and_spent_utxos(
semantically_verified: SemanticallyVerifiedBlock,
mut spent_outputs: HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
) -> Result<Self, ValueBalanceError> {
let SemanticallyVerifiedBlock {
block,
hash,
height,
new_outputs,
transaction_hashes,
deferred_balance,
} = semantically_verified;
spent_outputs.extend(new_outputs.clone());
Ok(Self {
block: block.clone(),
hash,
height,
new_outputs,
spent_outputs: spent_outputs.clone(),
transaction_hashes,
chain_value_pool_change: block.chain_value_pool_change(
&utxos_from_ordered_utxos(spent_outputs),
deferred_balance,
)?,
})
}
}
impl CheckpointVerifiedBlock {
pub fn new(
block: Arc<Block>,
hash: Option<block::Hash>,
deferred_balance: Option<Amount<NonNegative>>,
) -> Self {
let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash()));
block.deferred_balance = deferred_balance;
block
}
pub fn with_hash(block: Arc<Block>, hash: block::Hash) -> Self {
Self(SemanticallyVerifiedBlock::with_hash(block, hash))
}
}
impl SemanticallyVerifiedBlock {
pub fn with_hash(block: Arc<Block>, hash: block::Hash) -> Self {
let height = block
.coinbase_height()
.expect("semantically verified block should have a coinbase height");
let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect();
let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes);
Self {
block,
hash,
height,
new_outputs,
transaction_hashes,
deferred_balance: None,
}
}
pub fn with_deferred_balance(mut self, deferred_balance: Option<Amount<NonNegative>>) -> Self {
self.deferred_balance = deferred_balance;
self
}
}
impl From<Arc<Block>> for CheckpointVerifiedBlock {
fn from(block: Arc<Block>) -> Self {
CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block))
}
}
impl From<Arc<Block>> for SemanticallyVerifiedBlock {
fn from(block: Arc<Block>) -> Self {
let hash = block.hash();
let height = block
.coinbase_height()
.expect("semantically verified block should have a coinbase height");
let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect();
let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes);
Self {
block,
hash,
height,
new_outputs,
transaction_hashes,
deferred_balance: None,
}
}
}
impl From<ContextuallyVerifiedBlock> for SemanticallyVerifiedBlock {
fn from(valid: ContextuallyVerifiedBlock) -> Self {
Self {
block: valid.block,
hash: valid.hash,
height: valid.height,
new_outputs: valid.new_outputs,
transaction_hashes: valid.transaction_hashes,
deferred_balance: Some(
valid
.chain_value_pool_change
.deferred_amount()
.constrain::<NonNegative>()
.expect("deferred balance in a block must me non-negative"),
),
}
}
}
impl From<FinalizedBlock> for SemanticallyVerifiedBlock {
fn from(finalized: FinalizedBlock) -> Self {
Self {
block: finalized.block,
hash: finalized.hash,
height: finalized.height,
new_outputs: finalized.new_outputs,
transaction_hashes: finalized.transaction_hashes,
deferred_balance: finalized.deferred_balance,
}
}
}
impl From<CheckpointVerifiedBlock> for SemanticallyVerifiedBlock {
fn from(checkpoint_verified: CheckpointVerifiedBlock) -> Self {
checkpoint_verified.0
}
}
impl Deref for CheckpointVerifiedBlock {
type Target = SemanticallyVerifiedBlock;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CheckpointVerifiedBlock {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Request {
CommitSemanticallyVerifiedBlock(SemanticallyVerifiedBlock),
CommitCheckpointVerifiedBlock(CheckpointVerifiedBlock),
Depth(block::Hash),
Tip,
BlockLocator,
Transaction(transaction::Hash),
UnspentBestChainUtxo(transparent::OutPoint),
Block(HashOrHeight),
BlockAndSize(HashOrHeight),
BlockHeader(HashOrHeight),
AwaitUtxo(transparent::OutPoint),
FindBlockHashes {
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
},
FindBlockHeaders {
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
},
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
BestChainNextMedianTimePast,
BestChainBlockHash(block::Height),
KnownBlock(block::Hash),
CheckBlockProposalValidity(SemanticallyVerifiedBlock),
}
impl Request {
fn variant_name(&self) -> &'static str {
match self {
Request::CommitSemanticallyVerifiedBlock(_) => "commit_semantically_verified_block",
Request::CommitCheckpointVerifiedBlock(_) => "commit_checkpoint_verified_block",
Request::AwaitUtxo(_) => "await_utxo",
Request::Depth(_) => "depth",
Request::Tip => "tip",
Request::BlockLocator => "block_locator",
Request::Transaction(_) => "transaction",
Request::UnspentBestChainUtxo { .. } => "unspent_best_chain_utxo",
Request::Block(_) => "block",
Request::BlockAndSize(_) => "block_and_size",
Request::BlockHeader(_) => "block_header",
Request::FindBlockHashes { .. } => "find_block_hashes",
Request::FindBlockHeaders { .. } => "find_block_headers",
Request::CheckBestChainTipNullifiersAndAnchors(_) => {
"best_chain_tip_nullifiers_anchors"
}
Request::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
Request::BestChainBlockHash(_) => "best_chain_block_hash",
Request::KnownBlock(_) => "known_block",
Request::CheckBlockProposalValidity(_) => "check_block_proposal_validity",
}
}
pub fn count_metric(&self) {
metrics::counter!(
"state.requests",
"service" => "state",
"type" => self.variant_name()
)
.increment(1);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReadRequest {
UsageInfo,
Tip,
TipPoolValues,
Depth(block::Hash),
Block(HashOrHeight),
BlockAndSize(HashOrHeight),
BlockHeader(HashOrHeight),
Transaction(transaction::Hash),
TransactionIdsForBlock(HashOrHeight),
UnspentBestChainUtxo(transparent::OutPoint),
AnyChainUtxo(transparent::OutPoint),
BlockLocator,
FindBlockHashes {
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
},
FindBlockHeaders {
known_blocks: Vec<block::Hash>,
stop: Option<block::Hash>,
},
SaplingTree(HashOrHeight),
OrchardTree(HashOrHeight),
SaplingSubtrees {
start_index: NoteCommitmentSubtreeIndex,
limit: Option<NoteCommitmentSubtreeIndex>,
},
OrchardSubtrees {
start_index: NoteCommitmentSubtreeIndex,
limit: Option<NoteCommitmentSubtreeIndex>,
},
AddressBalance(HashSet<transparent::Address>),
TransactionIdsByAddresses {
addresses: HashSet<transparent::Address>,
height_range: RangeInclusive<block::Height>,
},
#[cfg(feature = "indexer")]
SpendingTransactionId(Spend),
UtxosByAddresses(HashSet<transparent::Address>),
CheckBestChainTipNullifiersAndAnchors(UnminedTx),
BestChainNextMedianTimePast,
BestChainBlockHash(block::Height),
ChainInfo,
SolutionRate {
num_blocks: usize,
height: Option<block::Height>,
},
CheckBlockProposalValidity(SemanticallyVerifiedBlock),
TipBlockSize,
}
impl ReadRequest {
fn variant_name(&self) -> &'static str {
match self {
ReadRequest::UsageInfo => "usage_info",
ReadRequest::Tip => "tip",
ReadRequest::TipPoolValues => "tip_pool_values",
ReadRequest::Depth(_) => "depth",
ReadRequest::Block(_) => "block",
ReadRequest::BlockAndSize(_) => "block_and_size",
ReadRequest::BlockHeader(_) => "block_header",
ReadRequest::Transaction(_) => "transaction",
ReadRequest::TransactionIdsForBlock(_) => "transaction_ids_for_block",
ReadRequest::UnspentBestChainUtxo { .. } => "unspent_best_chain_utxo",
ReadRequest::AnyChainUtxo { .. } => "any_chain_utxo",
ReadRequest::BlockLocator => "block_locator",
ReadRequest::FindBlockHashes { .. } => "find_block_hashes",
ReadRequest::FindBlockHeaders { .. } => "find_block_headers",
ReadRequest::SaplingTree { .. } => "sapling_tree",
ReadRequest::OrchardTree { .. } => "orchard_tree",
ReadRequest::SaplingSubtrees { .. } => "sapling_subtrees",
ReadRequest::OrchardSubtrees { .. } => "orchard_subtrees",
ReadRequest::AddressBalance { .. } => "address_balance",
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addresses",
ReadRequest::UtxosByAddresses(_) => "utxos_by_addresses",
ReadRequest::CheckBestChainTipNullifiersAndAnchors(_) => {
"best_chain_tip_nullifiers_anchors"
}
ReadRequest::BestChainNextMedianTimePast => "best_chain_next_median_time_past",
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
#[cfg(feature = "indexer")]
ReadRequest::SpendingTransactionId(_) => "spending_transaction_id",
ReadRequest::ChainInfo => "chain_info",
ReadRequest::SolutionRate { .. } => "solution_rate",
ReadRequest::CheckBlockProposalValidity(_) => "check_block_proposal_validity",
ReadRequest::TipBlockSize => "tip_block_size",
}
}
pub fn count_metric(&self) {
metrics::counter!(
"state.requests",
"service" => "read_state",
"type" => self.variant_name()
)
.increment(1);
}
}
impl TryFrom<Request> for ReadRequest {
type Error = &'static str;
fn try_from(request: Request) -> Result<ReadRequest, Self::Error> {
match request {
Request::Tip => Ok(ReadRequest::Tip),
Request::Depth(hash) => Ok(ReadRequest::Depth(hash)),
Request::BestChainNextMedianTimePast => Ok(ReadRequest::BestChainNextMedianTimePast),
Request::BestChainBlockHash(hash) => Ok(ReadRequest::BestChainBlockHash(hash)),
Request::Block(hash_or_height) => Ok(ReadRequest::Block(hash_or_height)),
Request::BlockAndSize(hash_or_height) => Ok(ReadRequest::BlockAndSize(hash_or_height)),
Request::BlockHeader(hash_or_height) => Ok(ReadRequest::BlockHeader(hash_or_height)),
Request::Transaction(tx_hash) => Ok(ReadRequest::Transaction(tx_hash)),
Request::UnspentBestChainUtxo(outpoint) => {
Ok(ReadRequest::UnspentBestChainUtxo(outpoint))
}
Request::BlockLocator => Ok(ReadRequest::BlockLocator),
Request::FindBlockHashes { known_blocks, stop } => {
Ok(ReadRequest::FindBlockHashes { known_blocks, stop })
}
Request::FindBlockHeaders { known_blocks, stop } => {
Ok(ReadRequest::FindBlockHeaders { known_blocks, stop })
}
Request::CheckBestChainTipNullifiersAndAnchors(tx) => {
Ok(ReadRequest::CheckBestChainTipNullifiersAndAnchors(tx))
}
Request::CommitSemanticallyVerifiedBlock(_)
| Request::CommitCheckpointVerifiedBlock(_) => Err("ReadService does not write blocks"),
Request::AwaitUtxo(_) => Err("ReadService does not track pending UTXOs. \
Manually convert the request to ReadRequest::AnyChainUtxo, \
and handle pending UTXOs"),
Request::KnownBlock(_) => Err("ReadService does not track queued blocks"),
Request::CheckBlockProposalValidity(semantically_verified) => Ok(
ReadRequest::CheckBlockProposalValidity(semantically_verified),
),
}
}
}