use miden_node_db::{DatabaseError, Db};
use miden_node_utils::tracing::OpenTelemetrySpanExt;
use miden_protocol::block::{BlockHeader, BlockNumber, ProposedBlock};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::Signature;
use miden_protocol::errors::ProposedBlockError;
use miden_protocol::transaction::{TransactionHeader, TransactionId};
use tracing::{Span, instrument};
use crate::db::{find_unvalidated_transactions, load_block_header};
use crate::{COMPONENT, ValidatorSigner};
#[derive(thiserror::Error, Debug)]
pub enum BlockValidationError {
#[error("block contains unvalidated transactions {0:?}")]
UnvalidatedTransactions(Vec<TransactionId>),
#[error("failed to build block")]
BlockBuildingFailed(#[source] ProposedBlockError),
#[error("failed to sign block: {0}")]
BlockSigningFailed(String),
#[error("failed to select transactions")]
DatabaseError(#[source] DatabaseError),
#[error("block number mismatch: expected {expected}, got {actual}")]
BlockNumberMismatch {
expected: BlockNumber,
actual: BlockNumber,
},
#[error("previous block commitment does not match chain tip")]
PrevBlockCommitmentMismatch,
#[error("no previous block header available for chain tip overwrite")]
NoPrevBlockHeader,
}
#[instrument(target = COMPONENT, skip_all, err, fields(tip.number = chain_tip.block_num().as_u32()))]
pub async fn validate_block(
proposed_block: ProposedBlock,
signer: &ValidatorSigner,
db: &Db,
chain_tip: BlockHeader,
) -> Result<(Signature, BlockHeader), BlockValidationError> {
let proposed_tx_ids =
proposed_block.transactions().map(TransactionHeader::id).collect::<Vec<_>>();
let unvalidated_txs = db
.transact("find_unvalidated_transactions", move |conn| {
find_unvalidated_transactions(conn, &proposed_tx_ids)
})
.await
.map_err(BlockValidationError::DatabaseError)?;
if !unvalidated_txs.is_empty() {
return Err(BlockValidationError::UnvalidatedTransactions(unvalidated_txs));
}
let (proposed_header, _) = proposed_block
.into_header_and_body()
.map_err(BlockValidationError::BlockBuildingFailed)?;
let span = Span::current();
span.set_attribute("block.number", proposed_header.block_num().as_u32());
span.set_attribute("block.commitment", proposed_header.commitment());
let prev = if proposed_header.block_num() == chain_tip.block_num() {
let prev_block_num =
chain_tip.block_num().parent().ok_or(BlockValidationError::NoPrevBlockHeader)?;
db.query("load_block_header", move |conn| load_block_header(conn, prev_block_num))
.await
.map_err(BlockValidationError::DatabaseError)?
.ok_or(BlockValidationError::NoPrevBlockHeader)?
} else {
let expected_block_num = chain_tip.block_num().child();
if proposed_header.block_num() != expected_block_num {
return Err(BlockValidationError::BlockNumberMismatch {
expected: expected_block_num,
actual: proposed_header.block_num(),
});
}
chain_tip
};
if proposed_header.prev_block_commitment() != prev.commitment() {
return Err(BlockValidationError::PrevBlockCommitmentMismatch);
}
let signature = sign_header(signer, &proposed_header).await?;
Ok((signature, proposed_header))
}
#[instrument(target = COMPONENT, name = "sign_block", skip_all, err, fields(block.number = header.block_num().as_u32()))]
async fn sign_header(
signer: &ValidatorSigner,
header: &BlockHeader,
) -> Result<Signature, BlockValidationError> {
signer
.sign(header)
.await
.map_err(|err| BlockValidationError::BlockSigningFailed(err.to_string()))
}