use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::{AccountDelta, AccountHeader, AccountId};
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::crypto::merkle::mmr::{InOrderIndex, MmrPeaks};
use miden_protocol::note::{NoteId, Nullifier};
use miden_protocol::transaction::TransactionId;
use super::SyncSummary;
use crate::account::Account;
use crate::note::{NoteUpdateTracker, NoteUpdateType};
use crate::rpc::domain::transaction::TransactionInclusion;
use crate::transaction::{DiscardCause, TransactionRecord, TransactionStatus};
#[derive(Default)]
pub struct StateSyncUpdate {
pub block_num: BlockNumber,
pub block_updates: BlockUpdates,
pub note_updates: NoteUpdateTracker,
pub transaction_updates: TransactionUpdateTracker,
pub account_updates: AccountUpdates,
}
impl From<&StateSyncUpdate> for SyncSummary {
fn from(value: &StateSyncUpdate) -> Self {
let new_public_note_ids = value
.note_updates
.updated_input_notes()
.filter_map(|note_update| {
let note = note_update.inner();
if let NoteUpdateType::Insert = note_update.update_type() {
Some(note.id())
} else {
None
}
})
.collect();
let committed_note_ids: BTreeSet<NoteId> = value
.note_updates
.updated_input_notes()
.filter_map(|note_update| {
let note = note_update.inner();
if let NoteUpdateType::Update = note_update.update_type() {
note.is_committed().then_some(note.id())
} else {
None
}
})
.chain(value.note_updates.updated_output_notes().filter_map(|note_update| {
let note = note_update.inner();
if let NoteUpdateType::Update = note_update.update_type() {
note.is_committed().then_some(note.id())
} else {
None
}
}))
.collect();
let consumed_note_ids: BTreeSet<NoteId> = value
.note_updates
.updated_input_notes()
.filter_map(|note| note.inner().is_consumed().then_some(note.inner().id()))
.collect();
SyncSummary::new(
value.block_num,
new_public_note_ids,
committed_note_ids.into_iter().collect(),
consumed_note_ids.into_iter().collect(),
value
.account_updates
.updated_public_accounts()
.iter()
.map(PublicAccountUpdate::id)
.collect(),
value
.account_updates
.mismatched_private_accounts()
.iter()
.map(|(id, _)| *id)
.collect(),
value.transaction_updates.committed_transactions().map(|t| t.id).collect(),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct BlockUpdates {
block_headers: BTreeMap<BlockNumber, (BlockHeader, bool, MmrPeaks)>,
new_authentication_nodes: Vec<(InOrderIndex, Word)>,
}
impl BlockUpdates {
pub fn insert(
&mut self,
block_header: BlockHeader,
has_client_notes: bool,
peaks: MmrPeaks,
new_authentication_nodes: Vec<(InOrderIndex, Word)>,
) {
debug_assert_eq!(
peaks.forest().num_leaves(),
block_header.block_num().as_usize(),
"MMR peaks stored for a block header must use that block number as the forest",
);
self.block_headers
.entry(block_header.block_num())
.and_modify(|(_, existing_has_notes, _)| {
*existing_has_notes |= has_client_notes;
})
.or_insert((block_header, has_client_notes, peaks));
self.new_authentication_nodes.extend(new_authentication_nodes);
}
pub fn block_headers(&self) -> impl Iterator<Item = &(BlockHeader, bool, MmrPeaks)> {
self.block_headers.values()
}
pub fn extend_authentication_nodes(&mut self, nodes: Vec<(InOrderIndex, Word)>) {
self.new_authentication_nodes.extend(nodes);
}
pub fn new_authentication_nodes(&self) -> &[(InOrderIndex, Word)] {
&self.new_authentication_nodes
}
}
#[derive(Default)]
pub struct TransactionUpdateTracker {
transactions: BTreeMap<TransactionId, TransactionRecord>,
external_nullifier_accounts: BTreeMap<Nullifier, AccountId>,
}
impl TransactionUpdateTracker {
pub fn new(transactions: Vec<TransactionRecord>) -> Self {
let transactions =
transactions.into_iter().map(|tx| (tx.id, tx)).collect::<BTreeMap<_, _>>();
Self {
transactions,
external_nullifier_accounts: BTreeMap::new(),
}
}
pub fn committed_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
self.transactions
.values()
.filter(|tx| matches!(tx.status, TransactionStatus::Committed { .. }))
}
pub fn discarded_transactions(&self) -> impl Iterator<Item = &TransactionRecord> {
self.transactions
.values()
.filter(|tx| matches!(tx.status, TransactionStatus::Discarded(_)))
}
fn mutable_pending_transactions(&mut self) -> impl Iterator<Item = &mut TransactionRecord> {
self.transactions
.values_mut()
.filter(|tx| matches!(tx.status, TransactionStatus::Pending))
}
pub fn updated_transaction_ids(&self) -> impl Iterator<Item = TransactionId> {
self.committed_transactions()
.chain(self.discarded_transactions())
.map(|tx| tx.id)
}
pub fn external_nullifier_account(&self, nullifier: &Nullifier) -> Option<AccountId> {
self.external_nullifier_accounts.get(nullifier).copied()
}
pub fn apply_transaction_inclusion(
&mut self,
transaction_inclusion: &TransactionInclusion,
timestamp: u64,
) {
if let Some(transaction) = self.transactions.get_mut(&transaction_inclusion.transaction_id)
{
transaction.commit_transaction(transaction_inclusion.block_num, timestamp);
return;
}
if let Some(transaction) = self.transactions.values_mut().find(|tx| {
tx.details.account_id == transaction_inclusion.account_id
&& tx.details.init_account_state == transaction_inclusion.initial_state_commitment
}) {
transaction.commit_transaction(transaction_inclusion.block_num, timestamp);
return;
}
for nullifier in &transaction_inclusion.nullifiers {
self.external_nullifier_accounts
.insert(*nullifier, transaction_inclusion.account_id);
}
}
pub fn apply_sync_height_update(
&mut self,
new_sync_height: BlockNumber,
tx_discard_delta: Option<u32>,
) {
if let Some(tx_discard_delta) = tx_discard_delta {
self.discard_transaction_with_predicate(
|transaction| {
transaction.details.submission_height
< new_sync_height.checked_sub(tx_discard_delta).unwrap_or_default()
},
DiscardCause::Stale,
);
}
self.discard_transaction_with_predicate(
|transaction| transaction.details.expiration_block_num <= new_sync_height,
DiscardCause::Expired,
);
}
pub fn apply_input_note_nullified(&mut self, input_note_nullifier: Nullifier) {
self.discard_transaction_with_predicate(
|transaction| {
transaction
.details
.input_note_nullifiers
.contains(&input_note_nullifier.as_word())
},
DiscardCause::InputConsumed,
);
}
pub fn apply_invalid_initial_account_state(&mut self, invalid_account_state: Word) {
self.discard_transaction_with_predicate(
|transaction| transaction.details.init_account_state == invalid_account_state,
DiscardCause::DiscardedInitialState,
);
}
fn discard_transaction_with_predicate<F>(&mut self, predicate: F, discard_cause: DiscardCause)
where
F: Fn(&TransactionRecord) -> bool,
{
let mut new_invalid_account_states = vec![];
for transaction in self.mutable_pending_transactions() {
if predicate(transaction) && transaction.discard_transaction(discard_cause) {
new_invalid_account_states.push(transaction.details.final_account_state);
}
}
for state in new_invalid_account_states {
self.apply_invalid_initial_account_state(state);
}
}
}
#[derive(Debug, Clone)]
pub enum PublicAccountUpdate {
Full(Account),
Delta {
new_header: AccountHeader,
delta: AccountDelta,
},
}
impl PublicAccountUpdate {
pub fn id(&self) -> AccountId {
match self {
Self::Full(account) => account.id(),
Self::Delta { new_header, .. } => new_header.id(),
}
}
}
#[derive(Debug, Clone, Default)]
#[allow(clippy::struct_field_names)]
pub struct AccountUpdates {
updated_public_accounts: Vec<PublicAccountUpdate>,
mismatched_private_accounts: Vec<(AccountId, Word)>,
}
impl AccountUpdates {
pub fn new(
updated_public_accounts: Vec<PublicAccountUpdate>,
mismatched_private_accounts: Vec<(AccountId, Word)>,
) -> Self {
Self {
updated_public_accounts,
mismatched_private_accounts,
}
}
pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
&self.updated_public_accounts
}
pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
&self.mismatched_private_accounts
}
pub fn extend(&mut self, other: AccountUpdates) {
self.updated_public_accounts.extend(other.updated_public_accounts);
self.mismatched_private_accounts.extend(other.mismatched_private_accounts);
}
}