diff --git a/applications/tari_ootle_app_utilities/src/fee_tables.rs b/applications/tari_ootle_app_utilities/src/fee_tables.rs index 862f7b9371..1d585c9db8 100644 --- a/applications/tari_ootle_app_utilities/src/fee_tables.rs +++ b/applications/tari_ootle_app_utilities/src/fee_tables.rs @@ -37,6 +37,18 @@ //! - **`per_byte_storage_cost`**: Cost per byte of data written to persistent storage (substates). Note: The actual //! cost is reduced by a factor of 4 (cost × 0.25) to make storage more affordable. //! +//! ## Template Publishing Fees +//! +//! A published template's binary is priced by its own model instead of the flat per-byte storage rate, since it is +//! the largest single payload a caller can commit to permanent state: +//! +//! - `per_template_publish_cost` — flat, charged once per publish. +//! - `template_size_premium_free_bytes` — priced at `per_byte_storage_cost`, no premium. +//! - `per_template_size_premium_unit_cost × units²` — where `units = (size − free) / template_size_premium_unit_bytes`. +//! +//! The quadratic term is what makes oversized templates expensive; the flat term sets the floor, because a small +//! template's size is dominated by fixed `template_lib` machinery rather than by anything its author wrote. +//! //! ## Cryptographic Operation Fees //! //! - **`per_signature_verification_cost`**: Cost for each cryptographic signature verification performed during @@ -81,14 +93,20 @@ const TESTNET_FEE_TABLE: FeeTable = FeeTable { template_load_bytes_cost_divisor: 3000, // 1 µT per 1000 Wasmer points. Lower values make metering more aggressive. wasm_points_cost_divisor: 1000, - // First 30 KiB of a template binary are priced at the per-byte storage rate; beyond that the - // quadratic publish premium applies. Keeps small/typical templates as cheap as before. - template_size_premium_free_bytes: 30 * 1024, + // First 96 KiB of a template binary are priced at the per-byte storage rate; beyond that the + // quadratic publish premium applies. A published template carries ~25 KiB of fixed + // `template_lib` machinery before any author code, and a minimal component template optimised + // the way the publish path optimises it lands around 76 KiB, so the allowance sits just above + // the floor: the premium prices author content, not library overhead. + template_size_premium_free_bytes: 96 * 1024, // 1 KiB per premium unit. template_size_premium_unit_bytes: 1024, - // 100 µT per unit². e.g. a 64 KiB template (34 units over the free allowance) pays - // 34² × 100 ≈ 0.12 tTARI premium; a 512 KiB template pays ≈ 23.2 tTARI. + // 100 µT per unit². e.g. a 256 KiB template (160 units over the free allowance) pays + // 160² × 100 ≈ 2.6 tTARI premium; a 512 KiB template pays ≈ 17.3 tTARI. per_template_size_premium_unit_cost: 100, + // 250_000 µT flat per publish. The size premium alone cannot price a small template above the + // noise floor, since most of a small binary is library overhead rather than author content. + per_template_publish_cost: 250_000, }; /// MainNet fee table - production values. @@ -115,9 +133,10 @@ const MAINNET_FEE_TABLE: FeeTable = FeeTable { storage_cost_divisor: 1, template_load_bytes_cost_divisor: 3000, wasm_points_cost_divisor: 1000, - template_size_premium_free_bytes: 30 * 1024, + template_size_premium_free_bytes: 96 * 1024, template_size_premium_unit_bytes: 1024, per_template_size_premium_unit_cost: 100, + per_template_publish_cost: 250_000, }; /// Returns the appropriate fee table for the specified network. diff --git a/crates/engine/src/fees/fee_module.rs b/crates/engine/src/fees/fee_module.rs index 617415a7a6..5bbcff025b 100644 --- a/crates/engine/src/fees/fee_module.rs +++ b/crates/engine/src/fees/fee_module.rs @@ -24,12 +24,13 @@ impl FeeModule { } } - /// Raw storage metrics for persisting a published template's `binary_len`-byte binary: the - /// bytes priced at the per-byte storage rate (the first `template_size_premium_free_bytes`, so - /// small templates cost the same as ordinary storage) and the quadratic premium on each whole - /// unit of the excess above that. Returned raw — not pre-divided — so the caller can accumulate - /// across the transaction and apply `storage_cost_divisor` once at finalization, matching how - /// the other byte/point charges round against per-transaction totals. + /// Metrics for persisting a published template's `binary_len`-byte binary: the bytes priced at + /// the per-byte storage rate (the first `template_size_premium_free_bytes`, so small templates + /// cost the same as ordinary storage), and the microtari charged on top — the flat per-publish + /// cost plus the quadratic premium on each whole unit of the excess above the free allowance. + /// The byte count is returned raw — not pre-divided — so the caller can accumulate across the + /// transaction and apply `storage_cost_divisor` once at finalization, matching how the other + /// byte/point charges round against per-transaction totals. fn template_publish_metrics(&self, binary_len: usize) -> Result<(u64, u64), RuntimeModuleError> { let size = binary_len as u64; let free = self.fee_table.template_size_premium_free_bytes(); @@ -37,17 +38,18 @@ impl FeeModule { let base_bytes = size.min(free); let units = size.saturating_sub(free) / self.fee_table.template_size_premium_unit_bytes(); - let premium = units + let charge = units .checked_mul(units) .and_then(|squared| squared.checked_mul(self.fee_table.per_template_size_premium_unit_cost())) + .and_then(|premium| premium.checked_add(self.fee_table.per_template_publish_cost())) .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating template publish premium".to_string()))?; - Ok((base_bytes, premium)) + Ok((base_bytes, charge)) } #[cfg(test)] fn template_publish_cost(&self, binary_len: usize) -> Result { - let (base_bytes, premium) = self.template_publish_metrics(binary_len)?; + let (base_bytes, charge) = self.template_publish_metrics(binary_len)?; let base = self .fee_table .per_byte_storage_cost() @@ -56,7 +58,7 @@ impl FeeModule { RuntimeModuleError::Overflow("Overflow calculating template base storage cost".to_string()) })? / self.fee_table.storage_cost_divisor(); - base.checked_add(premium) + base.checked_add(charge) .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating template publish cost".to_string())) } } @@ -108,27 +110,27 @@ impl RuntimeModule for FeeModule { } fn on_before_finalize(&self, track: &mut StateTracker) -> Result<(), RuntimeModuleError> { - let (total_storage, template_base_bytes, template_premium) = track.with_substates_to_persist(|changes| { + let (total_storage, template_base_bytes, template_charge) = track.with_substates_to_persist(|changes| { let mut counter = ByteCounter::new(); let mut base_bytes = 0u64; - let mut premium = 0u64; + let mut charge = 0u64; for substate in changes.values() { // A published template's binary is priced by the dedicated base + quadratic publish // model, so keep it out of the flat per-byte storage tally. Accumulate the raw // metrics here and apply the storage divisor once below. if let SubstateValue::Template(template) = substate { - let (tpl_base_bytes, tpl_premium) = self.template_publish_metrics(template.binary.len())?; + let (tpl_base_bytes, tpl_charge) = self.template_publish_metrics(template.binary.len())?; base_bytes = base_bytes.checked_add(tpl_base_bytes).ok_or_else(|| { RuntimeModuleError::Overflow("Overflow accumulating template base bytes".to_string()) })?; - premium = premium.checked_add(tpl_premium).ok_or_else(|| { + charge = charge.checked_add(tpl_charge).ok_or_else(|| { RuntimeModuleError::Overflow("Overflow accumulating template publish premium".to_string()) })?; continue; } encode_into_writer(substate, &mut counter)?; } - Ok::<_, RuntimeModuleError>((counter.get(), base_bytes, premium)) + Ok::<_, RuntimeModuleError>((counter.get(), base_bytes, charge)) })?; let cost = self @@ -147,7 +149,7 @@ impl RuntimeModule for FeeModule { })? / self.fee_table.storage_cost_divisor(); let template_publish_cost = template_base_cost - .checked_add(template_premium) + .checked_add(template_charge) .ok_or_else(|| RuntimeModuleError::Overflow("Overflow calculating template publish cost".to_string()))?; if template_publish_cost > 0 { track.add_fee_charge(FeeSource::TemplatePublish, template_publish_cost); @@ -237,6 +239,7 @@ mod tests { template_size_premium_free_bytes: 30 * 1024, template_size_premium_unit_bytes: 1024, per_template_size_premium_unit_cost: 100, + per_template_publish_cost: 500, ..FeeTable::zero_rated() } } @@ -248,16 +251,21 @@ mod tests { } #[test] - fn at_or_below_the_free_allowance_is_linear() { - assert_eq!(publish_cost(20 * 1024), 20 * 1024); - assert_eq!(publish_cost(30 * 1024), 30 * 1024); + fn the_flat_cost_is_charged_whatever_the_size() { + assert_eq!(publish_cost(0), 500); + } + + #[test] + fn at_or_below_the_free_allowance_is_flat_plus_linear() { + assert_eq!(publish_cost(20 * 1024), 500 + 20 * 1024); + assert_eq!(publish_cost(30 * 1024), 500 + 30 * 1024); } #[test] fn above_the_free_allowance_adds_a_quadratic_premium() { // base = first 30 KiB linear; premium = units² × 100, units = excess_KiB. - assert_eq!(publish_cost(38 * 1024), 30 * 1024 + 8 * 8 * 100); - assert_eq!(publish_cost(64 * 1024), 30 * 1024 + 34 * 34 * 100); + assert_eq!(publish_cost(38 * 1024), 500 + 30 * 1024 + 8 * 8 * 100); + assert_eq!(publish_cost(64 * 1024), 500 + 30 * 1024 + 34 * 34 * 100); } #[test] diff --git a/crates/engine/src/fees/fee_table.rs b/crates/engine/src/fees/fee_table.rs index d639b12f8a..0e74ef3316 100644 --- a/crates/engine/src/fees/fee_table.rs +++ b/crates/engine/src/fees/fee_table.rs @@ -40,6 +40,11 @@ pub struct FeeTable { /// (`per_template_size_premium_unit_cost × units²`, `units = excess_bytes / /// template_size_premium_unit_bytes`). Set to 0 to disable the publish premium. pub per_template_size_premium_unit_cost: u64, + /// Flat cost charged once per published template, independent of its size. A template's binary + /// is mostly fixed `template_lib` overhead rather than author content, so size alone cannot + /// price a small publish above the noise floor; this sets that floor. Set to 0 to charge for + /// size only. + pub per_template_publish_cost: u64, } impl FeeTable { @@ -58,6 +63,7 @@ impl FeeTable { template_size_premium_free_bytes: 0, template_size_premium_unit_bytes: 1, per_template_size_premium_unit_cost: 0, + per_template_publish_cost: 0, } } @@ -112,6 +118,10 @@ impl FeeTable { pub fn per_template_size_premium_unit_cost(&self) -> u64 { self.per_template_size_premium_unit_cost } + + pub fn per_template_publish_cost(&self) -> u64 { + self.per_template_publish_cost + } } fn non_zero(divisor: u64) -> u64 { diff --git a/crates/template_test_tooling/src/template_test.rs b/crates/template_test_tooling/src/template_test.rs index 7a91c3266f..b998432930 100644 --- a/crates/template_test_tooling/src/template_test.rs +++ b/crates/template_test_tooling/src/template_test.rs @@ -249,9 +249,10 @@ impl TemplateTest { storage_cost_divisor: 1, template_load_bytes_cost_divisor: 3000, wasm_points_cost_divisor: 1000, - template_size_premium_free_bytes: 30 * 1024, + template_size_premium_free_bytes: 96 * 1024, template_size_premium_unit_bytes: 1024, per_template_size_premium_unit_cost: 100, + per_template_publish_cost: 250_000, }, key_seed: 1, auto_add_proofs_from_signers: true,