use std::pin::Pin;
use std::sync::Arc;
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::account::AccountId;
use miden_protocol::block::{BlockNumber, SignedBlock};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio_stream::StreamExt;
use crate::actor::ActorRequest;
use crate::chain_state::SharedChainState;
use crate::clients::RpcError;
use crate::committed_block::CommittedBlockEffects;
use crate::coordinator::Coordinator;
use crate::db::NtxDbWriter;
use crate::server::NtxBuilderRpcServer;
use crate::{LOG_TARGET, NtxBuilderConfig};
enum SteadyStateAction {
Block(Box<Option<Result<(SignedBlock, BlockNumber), RpcError>>>),
Request(Option<ActorRequest>),
Respawn(Option<miden_protocol::account::AccountId>),
Shutdown,
}
pub(crate) type BlockStream =
Pin<Box<dyn Stream<Item = Result<(SignedBlock, BlockNumber), RpcError>> + Send>>;
pub struct NetworkTransactionBuilder {
config: NtxBuilderConfig,
db: NtxDbWriter,
block_stream: BlockStream,
last_applied_block: BlockNumber,
chain: Arc<SharedChainState>,
coordinator: Coordinator,
actor_request_rx: mpsc::Receiver<ActorRequest>,
is_synced: bool,
}
impl NetworkTransactionBuilder {
pub(crate) fn new(
config: NtxBuilderConfig,
db: NtxDbWriter,
block_stream: BlockStream,
last_applied_block: BlockNumber,
chain: Arc<SharedChainState>,
coordinator: Coordinator,
actor_request_rx: mpsc::Receiver<ActorRequest>,
) -> Self {
Self {
config,
db,
block_stream,
last_applied_block,
chain,
coordinator,
actor_request_rx,
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) = 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).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;
}
}
let max_note_attempts = self.config.max_note_attempts;
let pending_accounts = self
.db
.accounts_with_pending_notes(max_note_attempts)
.await
.context("failed to load accounts with pending notes at catch-up")?;
info!(
target: LOG_TARGET,
"spawning actors for accounts with carry-over pending notes",
account.ids.count = pending_accounts.len()
);
for account_id in pending_accounts {
self.coordinator.spawn_actor_when_committed(account_id).await?;
}
loop {
let action = {
let block_stream = &mut self.block_stream;
let actor_request_rx = &mut self.actor_request_rx;
let coordinator = &mut self.coordinator;
tokio::select! {
() = shutdown.cancelled() => SteadyStateAction::Shutdown,
block = block_stream.next() => SteadyStateAction::Block(Box::new(block)),
request = actor_request_rx.recv() => SteadyStateAction::Request(request),
respawn = coordinator.next() => SteadyStateAction::Respawn(respawn?),
}
};
match action {
SteadyStateAction::Block(block) => {
let (block, committed_tip) =
(*block).context("block stream ended")?.context("block stream failed")?;
let (effects, sponsored_accounts) =
self.apply_committed_block_with_effects(block, committed_tip).await?;
self.coordinator.handle_committed_block(&effects, &sponsored_accounts).await?;
},
SteadyStateAction::Request(request) => {
let Some(request) = request else {
anyhow::bail!("actor request channel closed unexpectedly");
};
handle_actor_request(&self.db, request, self.config.max_note_attempts).await?;
},
SteadyStateAction::Respawn(respawn) => {
if let Some(account_id) = respawn {
info!(
target: LOG_TARGET,
"respawning actor that shut down with a pending notification",
account.id = account_id
);
self.coordinator.spawn_actor(account_id);
}
},
SteadyStateAction::Shutdown => {
self.coordinator.shutdown().await?;
return Ok(());
},
}
}
}
async fn next_block(&mut self) -> anyhow::Result<(SignedBlock, BlockNumber)> {
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,
) -> anyhow::Result<()> {
self.apply_committed_block_with_effects(block, committed_tip).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,
) -> anyhow::Result<(CommittedBlockEffects, Vec<AccountId>)> {
let header = block.header().clone();
let block_num = header.block_num();
let effects = CommittedBlockEffects::from_signed_block(&block);
self.chain.update_chain_tip(header, self.config.max_block_count);
let next_mmr = self.chain.current_mmr();
let effects_for_db = effects.clone();
let sponsored_accounts = self
.db
.apply_committed_block(effects_for_db, next_mmr)
.await
.context("failed to apply committed block to DB")?;
self.last_applied_block = block_num;
Ok((effects, sponsored_accounts))
}
}
async fn handle_actor_request(
db: &NtxDbWriter,
request: ActorRequest,
max_note_attempts: usize,
) -> anyhow::Result<()> {
match request {
ActorRequest::NotesFailed { failed_notes, block_num, ack_tx } => {
db.notes_failed(failed_notes, block_num)
.await
.context("failed to persist note failure")?;
let _ = ack_tx.send(());
},
ActorRequest::NotesDiscarded { nullifiers, block_num, ack_tx } => {
db.discard_notes(nullifiers, block_num, max_note_attempts)
.await
.context("failed to persist note discard")?;
let _ = ack_tx.send(());
},
ActorRequest::CacheNoteScript { script_root, script } => {
db.insert_note_scripts(script_root, script)
.await
.context("failed to cache note script")?;
},
}
Ok(())
}