use miden_protocol::block::BlockNumber;
use miden_protocol::note::{Note, Nullifier};
use miden_standards::note::AccountTargetNetworkNote;
#[derive(Debug, Clone)]
pub struct InflightNetworkNote {
note: AccountTargetNetworkNote,
attempt_count: usize,
last_attempt: Option<BlockNumber>,
}
impl InflightNetworkNote {
pub fn new(note: AccountTargetNetworkNote) -> Self {
Self {
note,
attempt_count: 0,
last_attempt: None,
}
}
pub fn from_parts(
note: AccountTargetNetworkNote,
attempt_count: usize,
last_attempt: Option<BlockNumber>,
) -> Self {
Self { note, attempt_count, last_attempt }
}
pub fn into_inner(self) -> AccountTargetNetworkNote {
self.note
}
pub fn to_inner(&self) -> &AccountTargetNetworkNote {
&self.note
}
pub fn attempt_count(&self) -> usize {
self.attempt_count
}
pub fn is_available(&self, block_num: BlockNumber) -> bool {
self.note.execution_hint().can_be_consumed(block_num).unwrap_or(true)
&& has_backoff_passed(block_num, self.last_attempt, self.attempt_count)
}
pub fn fail(&mut self, block_num: BlockNumber) {
self.last_attempt = Some(block_num);
self.attempt_count += 1;
}
pub fn nullifier(&self) -> Nullifier {
self.note.as_note().nullifier()
}
}
impl From<InflightNetworkNote> for Note {
fn from(value: InflightNetworkNote) -> Self {
value.into_inner().into_note()
}
}
#[expect(clippy::cast_precision_loss, clippy::cast_sign_loss)]
fn has_backoff_passed(
chain_tip: BlockNumber,
last_attempt: Option<BlockNumber>,
attempts: usize,
) -> bool {
if attempts == 0 {
return true;
}
let blocks_passed = last_attempt
.and_then(|last| chain_tip.checked_sub(last.as_u32()))
.unwrap_or_default();
let backoff_threshold = (0.25 * attempts as f64).exp().round() as usize;
blocks_passed.as_usize() > backoff_threshold
}
#[cfg(test)]
mod tests {
use miden_protocol::block::BlockNumber;
use super::has_backoff_passed;
#[rstest::rstest]
#[test]
#[case::all_zero(Some(BlockNumber::GENESIS), BlockNumber::GENESIS, 0, true)]
#[case::no_attempts(None, BlockNumber::GENESIS, 0, true)]
#[case::one_attempt(Some(BlockNumber::GENESIS), BlockNumber::from(2), 1, true)]
#[case::three_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(3), 3, true)]
#[case::ten_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(13), 10, true)]
#[case::twenty_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(149), 20, true)]
#[case::one_attempt_false(Some(BlockNumber::GENESIS), BlockNumber::from(1), 1, false)]
#[case::three_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(2), 3, false)]
#[case::ten_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(12), 10, false)]
#[case::twenty_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(148), 20, false)]
fn backoff_has_passed(
#[case] last_attempt_block_num: Option<BlockNumber>,
#[case] current_block_num: BlockNumber,
#[case] attempt_count: usize,
#[case] backoff_should_have_passed: bool,
) {
assert_eq!(
backoff_should_have_passed,
has_backoff_passed(current_block_num, last_attempt_block_num, attempt_count)
);
}
}