use std::pin::Pin;
use anyhow::Context;
use futures::Stream;
use miden_node_tracing::{info, miden_instrument};
use miden_node_utils::shutdown::CancellationToken;
use miden_node_utils::tasks::Tasks;
use miden_protocol::block::{BlockNumber, SignedBlock};
use miden_protocol::protocol_config::ProtocolConfig;
use tokio::net::TcpListener;
use tokio_stream::StreamExt;
use crate::chain_state::ChainState;
use crate::clients::{BlockSubscriptionEvent, RpcError};
use crate::committed_block::CommittedBlockEffects;
use crate::db::NtxDbWriter;
use crate::network_transaction::NetworkTransactionOutcome;
use crate::scheduler::Scheduler;
use crate::server::NtxBuilderRpcServer;
use crate::{LOG_TARGET, NtxBuilderConfig};
enum SteadyStateAction {
Block(Box<Option<Result<BlockSubscriptionEvent, RpcError>>>),
Completion(anyhow::Result<NetworkTransactionOutcome>),
Shutdown,
}
pub(crate) type BlockStream =
Pin<Box<dyn Stream<Item = Result<BlockSubscriptionEvent, RpcError>> + Send>>;
pub struct NetworkTransactionBuilder {
config: NtxBuilderConfig,
db: NtxDbWriter,
block_stream: BlockStream,
last_applied_block: BlockNumber,
chain: ChainState,
scheduler: Scheduler,
is_synced: bool,
}
impl NetworkTransactionBuilder {
pub(crate) fn new(
config: NtxBuilderConfig,
db: NtxDbWriter,
block_stream: BlockStream,
last_applied_block: BlockNumber,
chain: ChainState,
scheduler: Scheduler,
) -> Self {
Self {
config,
db,
block_stream,
last_applied_block,
chain,
scheduler,
is_synced: false,
}
}
pub fn is_synced(&self) -> bool {
self.is_synced
}
pub async fn run(
self,
listener: TcpListener,
shutdown: CancellationToken,
) -> anyhow::Result<()> {
let mut tasks = Tasks::new();
let server = NtxBuilderRpcServer::new(
self.db.reader(),
self.config.max_note_attempts,
self.config.grpc_timeout,
);
let server_shutdown = shutdown.clone();
tasks.spawn("grpc-server", async move {
server
.serve(listener, server_shutdown)
.await
.context("ntx-builder gRPC server failed")
});
tasks.spawn("event-loop", self.run_event_loop(shutdown.clone()));
tasks.join_next_or_cancelled(shutdown).await.context("ntx-builder task failed")
}
async fn run_event_loop(mut self, shutdown: CancellationToken) -> anyhow::Result<()> {
loop {
let (block, committed_tip, protocol_config) = tokio::select! {
() = shutdown.cancelled() => return Ok(()),
result = self.next_block() => result?,
};
let local_tip = block.header().block_num();
self.apply_committed_block(block, committed_tip, protocol_config).await?;
if local_tip == committed_tip {
self.is_synced = true;
info!(
target: LOG_TARGET,
"ntx-builder is now in sync",
block.number = committed_tip
);
break;
}
}
self.scheduler.dispatch(&self.chain).await?;
loop {
let action = {
let block_stream = &mut self.block_stream;
let scheduler = &mut self.scheduler;
tokio::select! {
() = shutdown.cancelled() => SteadyStateAction::Shutdown,
block = block_stream.next() => SteadyStateAction::Block(Box::new(block)),
completion = scheduler.next_completion() => {
SteadyStateAction::Completion(completion)
},
}
};
match action {
SteadyStateAction::Block(block) => {
let (block, committed_tip, protocol_config) =
(*block).context("block stream ended")?.context("block stream failed")?;
let effects = self
.apply_committed_block_with_effects(block, committed_tip, protocol_config)
.await?;
self.scheduler.handle_committed_block(&effects);
self.scheduler.dispatch(&self.chain).await?;
},
SteadyStateAction::Completion(outcome) => {
if self.scheduler.handle_completion(&self.db, outcome?).await? {
self.scheduler.dispatch(&self.chain).await?;
}
},
SteadyStateAction::Shutdown => {
self.scheduler.shutdown().await;
return Ok(());
},
}
}
}
async fn next_block(&mut self) -> anyhow::Result<BlockSubscriptionEvent> {
self.block_stream
.next()
.await
.context("block stream ended")?
.context("block stream failed")
}
async fn apply_committed_block(
&mut self,
block: SignedBlock,
committed_tip: BlockNumber,
protocol_config: Option<ProtocolConfig>,
) -> anyhow::Result<()> {
self.apply_committed_block_with_effects(block, committed_tip, protocol_config)
.await
.map(drop)
}
#[miden_instrument(
name = "ntx.builder.apply_committed_block",
fields(
block.number = block.header().block_num(),
tip.number = committed_tip,
),
)]
async fn apply_committed_block_with_effects(
&mut self,
block: SignedBlock,
committed_tip: BlockNumber,
protocol_config: Option<ProtocolConfig>,
) -> anyhow::Result<CommittedBlockEffects> {
let block_num = block.header().block_num();
let next_chain =
self.chain
.next_chain_tip(&block, protocol_config, self.config.max_block_count)?;
let effects = CommittedBlockEffects::from_signed_block(&block);
let effects_for_db = effects.clone();
persist_and_publish_chain_state(&self.db, &mut self.chain, effects_for_db, next_chain)
.await?;
self.last_applied_block = block_num;
Ok(effects)
}
}
async fn persist_and_publish_chain_state(
db: &NtxDbWriter,
chain: &mut ChainState,
effects: CommittedBlockEffects,
next_chain: ChainState,
) -> anyhow::Result<()> {
let next_mmr = next_chain.current_mmr();
db.apply_committed_block(effects, next_mmr)
.await
.context("failed to apply committed block to DB")?;
*chain = next_chain;
Ok(())
}
#[cfg(test)]
mod protocol_config_tests {
use miden_protocol::crypto::merkle::mmr::PartialMmr;
use miden_protocol::protocol_config::ProtocolConfig;
use super::persist_and_publish_chain_state;
use crate::chain_state::ChainState;
use crate::committed_block::CommittedBlockEffects;
use crate::db::queries::account_effect::NetworkAccountEffect;
use crate::db::test_setup;
use crate::test_utils::{mock_block_header, mock_network_account_update};
#[tokio::test]
async fn failed_database_write_does_not_publish_chain_snapshot() {
let (db, _dir) = test_setup().await;
let config = ProtocolConfig::mock();
let mut chain =
ChainState::new(mock_block_header(0_u32.into()), PartialMmr::default(), config);
let next_header = mock_block_header(1_u32.into());
let next = chain.next_tip_from_header(next_header.clone(), None, 4).unwrap();
let (account, details) = mock_network_account_update();
let effects = CommittedBlockEffects {
header: next_header,
network_notes: vec![],
sponsorship_notes: vec![],
nullifiers: vec![],
network_account_updates: vec![(
account.id(),
NetworkAccountEffect::from_account_creation(&details)
.expect("the mock account should be a network account"),
)],
account_transactions: vec![],
};
persist_and_publish_chain_state(&db, &mut chain, effects, next)
.await
.expect_err("a post-genesis account update without a transaction must fail");
assert_eq!(chain.chain_tip_header.block_num(), 0_u32.into());
}
}