use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use miden_node_db::DatabaseError;
use miden_node_db::sqlite::Database;
use miden_node_proto::domain::encryption::TransactionEncryptionKeyInfo;
use miden_node_store::BlockStore;
use miden_node_utils::tracing::{miden_instrument, miden_span_record};
use miden_protocol::Word;
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,
PrivateRecordChainId,
PrivateRecordSealer,
TransactionInputDecrypter,
ValidatorSigner,
};
#[cfg(test)]
mod tests;
mod block_subscription;
mod get_transaction_encryption_key;
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:?} is not a member of the validator set authorized to sign this block"
)]
ValidatorKeyNotInSet { actual: PublicKey },
#[error("no chain tip exists")]
NoChainTip,
#[error("failed to backup block")]
BlockBackupFailed(#[source] std::io::Error),
#[error("no genesis block header exists")]
NoGenesisHeader,
#[error("failed to attest the transaction encryption key: {0}")]
EncryptionKeyAttestationFailed(String),
}
pub(crate) struct InitialMetrics {
chain_tip: u32,
validated_transactions: u64,
signed_blocks: u64,
}
impl InitialMetrics {
pub(crate) const fn new(
chain_tip: u32,
validated_transactions: u64,
signed_blocks: u64,
) -> Self {
Self {
chain_tip,
validated_transactions,
signed_blocks,
}
}
}
pub(crate) struct ValidatorService {
signer: ValidatorSigner,
decrypter: Arc<dyn TransactionInputDecrypter>,
genesis_commitment: Word,
private_record_sealer: PrivateRecordSealer,
private_record_chain_id: PrivateRecordChainId,
encryption_key_info: TransactionEncryptionKeyInfo,
encryption_key_attestation: Signature,
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,
decrypter: Arc<dyn TransactionInputDecrypter>,
private_record_sealer: PrivateRecordSealer,
db: Database,
block_store: BlockStore,
initial_metrics: InitialMetrics,
) -> 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();
if !chain_tip.validator_keys().as_keys().contains(&signing_key) {
return Err(ValidatorError::ValidatorKeyNotInSet { actual: signing_key });
}
let genesis_commitment = db
.read("load_genesis_header", |tx| load_block_header(tx, BlockNumber::GENESIS))
.await
.map_err(ValidatorError::DatabaseError)?
.ok_or(ValidatorError::NoGenesisHeader)?
.commitment();
let private_record_chain_id = PrivateRecordChainId::new(
genesis_commitment
.to_bytes()
.try_into()
.expect("a Miden block commitment is always 32 bytes"),
);
let encryption_key_info = decrypter
.encryption_key()
.await
.map_err(|err| ValidatorError::EncryptionKeyAttestationFailed(err.to_string()))?;
let encryption_key_attestation = signer
.sign_commitment(encryption_key_info.attestation_commitment(genesis_commitment))
.await
.map_err(|err| ValidatorError::EncryptionKeyAttestationFailed(err.to_string()))?;
Ok(Self {
signer,
decrypter,
genesis_commitment,
private_record_sealer,
private_record_chain_id,
encryption_key_info,
encryption_key_attestation,
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_metrics.chain_tip)),
validated_transactions_count: AtomicU64::new(initial_metrics.validated_transactions),
signed_blocks_count: AtomicU64::new(initial_metrics.signed_blocks),
})
}
#[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();
if !prev.validator_keys().as_keys().contains(&signing_key) {
return Err(ValidatorError::ValidatorKeyNotInSet { actual: signing_key });
}
let signature = self.sign_header(&proposed_header).await?;
let own_signature = BlockSignatures::new(vec![signature.clone()])
.expect("a single signature is within the signature set bounds");
let signed_block =
SignedBlock::new_unchecked(proposed_header, proposed_body, own_signature);
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_commitment(header.commitment())
.await
.map_err(|err| ValidatorError::BlockSigningFailed(err.to_string()))
}
}