use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use miden_node_db::DatabaseError;
use miden_node_db::sqlite::Database;
use miden_node_store::BlockStore;
use miden_node_utils::tracing::{miden_instrument, miden_span_record};
use miden_protocol::block::{
BlockHeader,
BlockNumber,
BlockSignatures,
ProposedBlock,
SignedBlock,
};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::{PublicKey, Signature};
use miden_protocol::crypto::utils::Serializable;
use miden_protocol::errors::ProposedBlockError;
use miden_protocol::transaction::{TransactionHeader, TransactionId};
use tokio::sync::{Semaphore, watch};
use crate::db::{find_unvalidated_transactions, load_block_header, load_chain_tip};
use crate::{COMPONENT, ValidatorSigner};
#[cfg(test)]
mod tests;
mod block_subscription;
mod sign_block;
mod status;
mod submit_proven_transaction;
#[derive(thiserror::Error, Debug)]
pub enum ValidatorError {
#[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,
#[error(
"validator signing key {actual:?} does not match the block's validator key {expected:?}"
)]
ValidatorKeyMismatch { expected: PublicKey, actual: PublicKey },
#[error("no chain tip exists")]
NoChainTip,
#[error("failed to backup block")]
BlockBackupFailed(#[source] std::io::Error),
#[error("expected a single-key validator set, got {actual} keys")]
UnexpectedValidatorSetSize { actual: usize },
}
pub(crate) struct ValidatorService {
signer: ValidatorSigner,
db: Arc<Database>,
block_store: BlockStore,
serve_lock: Arc<tokio::sync::RwLock<()>>,
sign_block_semaphore: Semaphore,
committed_tip: watch::Sender<BlockNumber>,
validated_transactions_count: AtomicU64,
signed_blocks_count: AtomicU64,
}
impl ValidatorService {
pub(crate) async fn new(
signer: ValidatorSigner,
db: Database,
block_store: BlockStore,
initial_chain_tip: u32,
initial_tx_count: u64,
initial_block_count: u64,
) -> Result<Self, ValidatorError> {
let chain_tip = db
.read("load_chain_tip", load_chain_tip)
.await
.map_err(ValidatorError::DatabaseError)?
.ok_or(ValidatorError::NoChainTip)?;
let signing_key = signer.public_key();
let expected_key = match chain_tip.validator_keys().as_keys() {
[key] => key,
keys => {
return Err(ValidatorError::UnexpectedValidatorSetSize { actual: keys.len() });
},
};
if &signing_key != expected_key {
return Err(ValidatorError::ValidatorKeyMismatch {
expected: expected_key.clone(),
actual: signing_key,
});
}
Ok(Self {
signer,
serve_lock: Arc::new(tokio::sync::RwLock::new(())),
db: db.into(),
block_store,
sign_block_semaphore: Semaphore::new(1),
committed_tip: watch::Sender::new(BlockNumber::from(initial_chain_tip)),
validated_transactions_count: AtomicU64::new(initial_tx_count),
signed_blocks_count: AtomicU64::new(initial_block_count),
})
}
#[miden_instrument(
target = COMPONENT,
skip_all,
err,
)]
pub async fn validate_block(
&self,
proposed_block: ProposedBlock,
chain_tip: BlockHeader,
) -> Result<(Signature, BlockHeader), ValidatorError> {
miden_span_record!(tip.number = chain_tip.block_num().as_u32(),);
let proposed_tx_ids =
proposed_block.transactions().map(TransactionHeader::id).collect::<Vec<_>>();
let unvalidated_txs = self
.db
.read("find_unvalidated_transactions", move |tx| {
find_unvalidated_transactions(tx, &proposed_tx_ids)
})
.await
.map_err(ValidatorError::DatabaseError)?;
if !unvalidated_txs.is_empty() {
return Err(ValidatorError::UnvalidatedTransactions(unvalidated_txs));
}
let (proposed_header, proposed_body) = proposed_block
.into_header_and_body()
.map_err(ValidatorError::BlockBuildingFailed)?;
miden_span_record!(
block.number = proposed_header.block_num().as_u32(),
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(ValidatorError::NoPrevBlockHeader)?;
self.db
.read("load_block_header", move |tx| load_block_header(tx, prev_block_num))
.await
.map_err(ValidatorError::DatabaseError)?
.ok_or(ValidatorError::NoPrevBlockHeader)?
} else {
let expected_block_num = chain_tip.block_num().child();
if proposed_header.block_num() != expected_block_num {
return Err(ValidatorError::BlockNumberMismatch {
expected: expected_block_num,
actual: proposed_header.block_num(),
});
}
chain_tip
};
if proposed_header.prev_block_commitment() != prev.commitment() {
return Err(ValidatorError::PrevBlockCommitmentMismatch);
}
let signing_key = self.signer.public_key();
let expected_key = match proposed_header.validator_keys().as_keys() {
[key] => key,
keys => {
return Err(ValidatorError::UnexpectedValidatorSetSize { actual: keys.len() });
},
};
if &signing_key != expected_key {
return Err(ValidatorError::ValidatorKeyMismatch {
expected: expected_key.clone(),
actual: signing_key,
});
}
let signature = self.sign_header(&proposed_header).await?;
let signatures = BlockSignatures::new(vec![signature.clone()])
.map_err(|err| ValidatorError::BlockSigningFailed(err.to_string()))?;
let signed_block = SignedBlock::new_unchecked(proposed_header, proposed_body, signatures);
self.block_store
.save_block(signed_block.header().block_num(), &signed_block.to_bytes())
.await
.map_err(ValidatorError::BlockBackupFailed)?;
let (header, ..) = signed_block.into_parts();
Ok((signature, header))
}
#[miden_instrument(
target = COMPONENT,
name = "sign_block",
skip_all,
err,
fields(
block.number = header.block_num().as_u32(),
),
)]
async fn sign_header(&self, header: &BlockHeader) -> Result<Signature, ValidatorError> {
self.signer
.sign(header)
.await
.map_err(|err| ValidatorError::BlockSigningFailed(err.to_string()))
}
}