use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use miden_agglayer::AgglayerNote;
use miden_protocol::asset::AssetAmount;
use miden_protocol::block::FeeParameters;
use miden_protocol::errors::AssetError;
use miden_protocol::note::NoteScriptRoot;
use miden_protocol::transaction::{TransactionFee, TransactionFeeError};
use miden_standards::account::fees::{BasicConstantFeePolicy, FeePolicyManager};
use miden_standards::note::StandardNote;
use miden_standards::note::costs::NoteCost;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum NotePricingError {
#[error("cannot compute the fee for a note")]
Fee(#[source] TransactionFeeError),
#[error("accumulated note price overflows u64")]
PriceOverflow,
#[error("accumulated note price exceeds the maximum asset amount")]
PriceExceedsMaxAssetAmount(#[source] AssetError),
#[error("no consumption cost is known for note script root {0}")]
UnknownNoteScriptRoot(NoteScriptRoot),
}
fn resolve_note_cost(root: NoteScriptRoot) -> Option<NoteCost> {
StandardNote::note_cost(root).or_else(|| AgglayerNote::note_cost(root))
}
#[derive(Debug, Clone, bon::Builder)]
pub struct NetworkNotePricer {
#[builder(field)]
note_costs: BTreeMap<NoteScriptRoot, NoteCost>,
fee_parameters: FeeParameters,
#[builder(default = 1)]
safety_margin_verification_cycles: u32,
}
impl NetworkNotePricer {
pub fn fee_parameters(&self) -> &FeeParameters {
&self.fee_parameters
}
pub fn fee(&self, fee_inputs: TransactionFee) -> Result<AssetAmount, NotePricingError> {
fee_inputs
.with_safety_margin(self.safety_margin_verification_cycles)
.compute_fee(&self.fee_parameters)
.map_err(NotePricingError::Fee)
}
pub fn price(&self, root: NoteScriptRoot) -> Result<AssetAmount, NotePricingError> {
let price = self.price_recursive(root, &mut Vec::new())?;
AssetAmount::new(price).map_err(NotePricingError::PriceExceedsMaxAssetAmount)
}
pub fn basic_constant_fee_policy_manager(
&self,
note_script_roots: impl IntoIterator<Item = NoteScriptRoot>,
) -> Result<FeePolicyManager, NotePricingError> {
let mut policy = BasicConstantFeePolicy::new();
for root in note_script_roots {
policy = policy.with_fee(root, self.price(root)?);
}
Ok(FeePolicyManager::builder()
.fee_faucet_id(self.fee_parameters.fee_faucet_id())
.active_fee_policy(policy.into())
.build())
}
fn price_recursive(
&self,
root: NoteScriptRoot,
pricing_stack: &mut Vec<NoteScriptRoot>,
) -> Result<u64, NotePricingError> {
let cost = self
.note_costs
.get(&root)
.cloned()
.or_else(|| resolve_note_cost(root))
.ok_or(NotePricingError::UnknownNoteScriptRoot(root))?;
let fee_inputs = TransactionFee::new(cost.cycles()).map_err(NotePricingError::Fee)?;
let own_fee = self.fee(fee_inputs)?.as_u64();
if pricing_stack.contains(&root) {
return Ok(own_fee);
}
pricing_stack.push(root);
let mut total = own_fee;
for &created in cost.created_notes() {
let created_price = self.price_recursive(created, pricing_stack)?;
total = total.checked_add(created_price).ok_or(NotePricingError::PriceOverflow)?;
}
pricing_stack.pop();
Ok(total)
}
}
impl<S: network_note_pricer_builder::State> NetworkNotePricerBuilder<S> {
pub fn note_cost(mut self, root: NoteScriptRoot, cost: NoteCost) -> Self {
self.note_costs.insert(root, cost);
self
}
pub fn note_costs(
mut self,
note_costs: impl IntoIterator<Item = (NoteScriptRoot, NoteCost)>,
) -> Self {
self.note_costs.extend(note_costs);
self
}
}
#[cfg(test)]
mod tests {
use miden_agglayer::ClaimNote;
use miden_agglayer::costs::CLAIM_CONSUMPTION_CYCLES;
use miden_protocol::MAX_TX_EXECUTION_CYCLES;
use miden_protocol::account::AccountId;
use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
use miden_standards::note::costs::{
MINT_CONSUMPTION_CYCLES,
P2ID_CONSUMPTION_CYCLES,
SWAP_CONSUMPTION_CYCLES,
};
use miden_standards::note::{ConstantFeePolicyConfigNote, P2idNote, SwapNote};
use super::*;
fn fee_parameters(base_fee: u32) -> FeeParameters {
let fee_faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
.expect("testing faucet ID should be valid");
FeeParameters::new(fee_faucet_id, base_fee)
}
fn pricer(base_fee: u32, margin: u32) -> NetworkNotePricer {
NetworkNotePricer::builder()
.fee_parameters(fee_parameters(base_fee))
.safety_margin_verification_cycles(margin)
.build()
}
fn fee_inputs(cycles: u32) -> TransactionFee {
TransactionFee::new(cycles).expect("test cycle counts are non-zero")
}
#[test]
fn fee_implements_the_kernel_formula() {
let no_margin = pricer(500, 0);
assert_eq!(no_margin.fee(fee_inputs(1 << 16)).unwrap().as_u64(), 500 * 17);
assert_eq!(no_margin.fee(fee_inputs((1 << 17) - 1)).unwrap().as_u64(), 500 * 17);
assert_eq!(no_margin.fee(fee_inputs(1 << 17)).unwrap().as_u64(), 500 * 18);
assert_eq!(no_margin.fee(fee_inputs(1)).unwrap().as_u64(), 500);
}
#[test]
fn default_safety_margin_adds_one_verification_cycle() {
let default_margin =
NetworkNotePricer::builder().fee_parameters(fee_parameters(500)).build();
assert_eq!(default_margin.fee(fee_inputs(1 << 16)).unwrap().as_u64(), 500 * 18);
}
#[test]
fn out_of_range_cycle_costs_cannot_be_priced() {
let root = NoteScriptRoot::from_array([1, 0, 0, 0]);
for cycles in [0, u32::MAX] {
let broken = custom_pricer([(root, NoteCost::new(cycles, Vec::new()))]);
assert!(matches!(broken.price(root), Err(NotePricingError::Fee(_))));
}
}
#[test]
fn fee_exceeding_max_asset_amount_is_rejected() {
assert!(matches!(
pricer(u32::MAX, u32::MAX).fee(fee_inputs(MAX_TX_EXECUTION_CYCLES)),
Err(NotePricingError::Fee(_))
));
}
fn test_graph() -> [(NoteScriptRoot, NoteCost); 3] {
let self_recursive = NoteScriptRoot::from_array([1, 0, 0, 0]);
let parent = NoteScriptRoot::from_array([2, 0, 0, 0]);
let leaf = NoteScriptRoot::from_array([3, 0, 0, 0]);
[
(self_recursive, NoteCost::new(1 << 16, vec![parent, self_recursive])),
(parent, NoteCost::new(1 << 10, vec![leaf])),
(leaf, NoteCost::new(1 << 16, Vec::new())),
]
}
fn max_fee_pricer() -> NetworkNotePricer {
NetworkNotePricer::builder()
.fee_parameters(fee_parameters(u32::MAX))
.safety_margin_verification_cycles((1 << 31) - 17)
.note_costs(test_graph())
.build()
}
#[test]
fn overflowing_accumulated_price_is_rejected() {
assert!(matches!(
max_fee_pricer().price(NoteScriptRoot::from_array([1, 0, 0, 0])),
Err(NotePricingError::PriceOverflow)
));
}
#[test]
fn accumulated_price_above_max_asset_amount_is_rejected() {
assert!(matches!(
max_fee_pricer().price(NoteScriptRoot::from_array([2, 0, 0, 0])),
Err(NotePricingError::PriceExceedsMaxAssetAmount(_))
));
}
fn custom_pricer(
costs: impl IntoIterator<Item = (NoteScriptRoot, NoteCost)>,
) -> NetworkNotePricer {
NetworkNotePricer::builder()
.fee_parameters(fee_parameters(500))
.safety_margin_verification_cycles(0)
.note_costs(costs)
.build()
}
#[test]
fn price_includes_created_notes() {
let parent = NoteScriptRoot::from_array([2, 0, 0, 0]);
let expected = 500 * 11 + 500 * 17;
assert_eq!(custom_pricer(test_graph()).price(parent).unwrap().as_u64(), expected);
}
#[test]
fn self_recursive_notes_are_priced_at_one_level_of_nesting() {
let selfish = NoteScriptRoot::from_array([1, 0, 0, 0]);
let expected = 500 * 17 + (500 * 11 + 500 * 17) + 500 * 17;
assert_eq!(custom_pricer(test_graph()).price(selfish).unwrap().as_u64(), expected);
}
#[test]
fn unknown_roots_cannot_be_priced() {
let unknown = NoteScriptRoot::from_array([9, 9, 9, 9]);
assert!(matches!(
pricer(500, 0).price(unknown),
Err(NotePricingError::UnknownNoteScriptRoot(root)) if root == unknown
));
}
#[test]
fn supplied_note_costs_extend_the_built_in_tables() {
let custom = NoteScriptRoot::from_array([7, 0, 0, 0]);
let pricer =
custom_pricer([(custom, NoteCost::new(1 << 16, vec![P2idNote::script_root()]))]);
let expected = pricer.fee(fee_inputs(1 << 16)).unwrap().as_u64()
+ pricer.fee(fee_inputs(P2ID_CONSUMPTION_CYCLES)).unwrap().as_u64();
assert_eq!(pricer.price(custom).unwrap().as_u64(), expected);
}
#[test]
fn individual_note_costs_can_be_supplied_one_at_a_time() {
let first = NoteScriptRoot::from_array([7, 0, 0, 0]);
let second = NoteScriptRoot::from_array([8, 0, 0, 0]);
let pricer = NetworkNotePricer::builder()
.fee_parameters(fee_parameters(500))
.safety_margin_verification_cycles(0)
.note_cost(first, NoteCost::new(1 << 16, Vec::new()))
.note_cost(second, NoteCost::new(1 << 10, Vec::new()))
.build();
assert_eq!(
pricer.price(first).unwrap().as_u64(),
pricer.fee(fee_inputs(1 << 16)).unwrap().as_u64()
);
assert_eq!(
pricer.price(second).unwrap().as_u64(),
pricer.fee(fee_inputs(1 << 10)).unwrap().as_u64()
);
}
#[test]
fn supplied_note_costs_shadow_the_built_in_tables() {
let root = SwapNote::script_root();
let pricer =
custom_pricer([(root, NoteCost::new(2 * SWAP_CONSUMPTION_CYCLES, Vec::new()))]);
let expected = pricer.fee(fee_inputs(2 * SWAP_CONSUMPTION_CYCLES)).unwrap().as_u64();
assert_eq!(pricer.price(root).unwrap().as_u64(), expected);
}
#[test]
fn swap_price_includes_the_p2id_payback_leg() {
let pricer = pricer(500, 0);
let p2id_fee = pricer.fee(fee_inputs(P2ID_CONSUMPTION_CYCLES)).unwrap().as_u64();
let swap_fee = pricer.fee(fee_inputs(SWAP_CONSUMPTION_CYCLES)).unwrap().as_u64();
assert_eq!(pricer.price(SwapNote::script_root()).unwrap().as_u64(), swap_fee + p2id_fee);
}
#[test]
fn claim_price_includes_the_mint_and_p2id_legs() {
let pricer = pricer(500, 0);
let expected = pricer.fee(fee_inputs(CLAIM_CONSUMPTION_CYCLES)).unwrap().as_u64()
+ pricer.fee(fee_inputs(MINT_CONSUMPTION_CYCLES)).unwrap().as_u64()
+ pricer.fee(fee_inputs(P2ID_CONSUMPTION_CYCLES)).unwrap().as_u64();
assert_eq!(pricer.price(ClaimNote::script_root()).unwrap().as_u64(), expected);
}
#[test]
fn basic_constant_fee_policy_manager_prices_every_root_in_the_native_fee_asset() {
let pricer = pricer(500, 0);
let roots = [
SwapNote::script_root(),
ClaimNote::script_root(),
ConstantFeePolicyConfigNote::script_root(),
];
let manager = pricer.basic_constant_fee_policy_manager(roots).unwrap();
assert_eq!(manager.active_fee_policy(), BasicConstantFeePolicy::root());
assert_eq!(
manager.fee_asset_id(),
miden_protocol::asset::AssetId::new_fungible(pricer.fee_parameters().fee_faucet_id())
);
}
#[test]
fn basic_constant_fee_policy_manager_rejects_unknown_roots() {
let unknown = NoteScriptRoot::from_array([9, 9, 9, 9]);
assert!(matches!(
pricer(500, 0).basic_constant_fee_policy_manager([unknown]),
Err(NotePricingError::UnknownNoteScriptRoot(root)) if root == unknown
));
}
}