From 554f6cebf4478107800e20b0f7af3963911abcf5 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 24 Aug 2026 12:24:48 +0200 Subject: [PATCH 1/2] feat(dpp)!: rebalance the shielded fee constants for protocol 14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat shielded fee's components were not independently sufficient: the formula reserved 100M credits for Halo 2 proof verification and 22M/action for per-action compute, but allotted only 344 bytes/action (9.43M credits) of storage while the GROVE_V4 fixed per-append model meters a 1-action transfer at 17.88M credits (14.34M storage = 523 bytes: the declared payload plus Merk framing, dense path records and the amortized chunk framing) — so the compute budget silently subsidized database work. Protocol 14 (DriveAbciValidationConstants v10, protocol-14-only): - `shielded_proof_verification_fee` 100M -> 40M: ~5 ms of Halo 2 at the fee model's ~8M credits/ms, ~27x a BLS signature verification — reserved for compute alone. - new table field `shielded_storage_bytes_per_action`, 344 (the declared payload, locked) in v1..v9 and 550 at v10, sized to the measured footprint with headroom, so the storage component alone covers the database work. - `compute_minimum_shielded_fee_v0` reads the allowance from the table; no method version bump — the logic is unchanged and released versions carry the shipped value byte-for-byte. Net flat fees at protocol 14 (2 actions): transfer 162.85M -> 114.14M (-30%), unshield -> 120.22M, withdrawal -> 226.48M. Protocol 13 fees are untouched. The mainnet-halt-repro test pins itself to protocol 13, whose fee constants its measured funding band belongs to. Co-Authored-By: Claude Fable 5 --- .../compute_minimum_shielded_fee/v0/mod.rs | 38 +++++++++++-------- packages/rs-dpp/src/shielded/mod.rs | 28 -------------- .../state_transitions/shield/tests.rs | 10 ++++- .../shielded_transfer/tests.rs | 2 +- .../drive_abci_validation_versions/mod.rs | 14 +++++++ .../drive_abci_validation_versions/v1.rs | 3 ++ .../drive_abci_validation_versions/v10.rs | 16 +++++++- .../drive_abci_validation_versions/v2.rs | 3 ++ .../drive_abci_validation_versions/v3.rs | 3 ++ .../drive_abci_validation_versions/v4.rs | 3 ++ .../drive_abci_validation_versions/v5.rs | 3 ++ .../drive_abci_validation_versions/v6.rs | 3 ++ .../drive_abci_validation_versions/v7.rs | 3 ++ .../drive_abci_validation_versions/v8.rs | 3 ++ .../drive_abci_validation_versions/v9.rs | 3 ++ .../src/shielded_send.rs | 6 +-- 16 files changed, 90 insertions(+), 51 deletions(-) diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index d59a4d3aaf0..533ba0c6182 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -1,7 +1,6 @@ use crate::fee::Credits; use crate::shielded::{ - SHIELDED_STORAGE_BYTES_PER_ACTION, SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES, - SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES, + SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES, SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES, }; use crate::ProtocolError; use platform_version::version::PlatformVersion; @@ -50,12 +49,11 @@ pub fn compute_shielded_verification_fee_v0( /// /// where `compute_fee = proof_verification_fee + num_actions × processing_fee` /// (see [`compute_shielded_verification_fee_v0`]) and -/// `storage_fee_per_action = SHIELDED_STORAGE_BYTES_PER_ACTION × (disk + processing) credits/byte`. -/// -/// Expanding, this equals the historical formula -/// `proof_verification_fee + num_actions × (processing_fee + 312×rate)`: the compute fee already -/// contributes `proof + num_actions × processing`, and this function adds -/// `num_actions × (312×rate)` storage on top, so the returned numeric value is unchanged. +/// `storage_fee_per_action = shielded_storage_bytes_per_action × (disk + processing) credits/byte`, +/// with the byte allowance a versioned event constant beside the compute fees — so the compute +/// and storage components of the flat fee are calibrated independently per protocol version +/// (compute reserved for compute; the allowance sized to what the metering actually charges a +/// note/nullifier write under the GroveVersion in force). /// /// This is the fee carved from the shielded **pool** by the pool-paid transitions /// (ShieldedTransfer / Unshield / ShieldedWithdrawal), which cannot meter their writes against an @@ -69,6 +67,10 @@ pub fn compute_minimum_shielded_fee_v0( platform_version: &PlatformVersion, ) -> Result { let storage = &platform_version.fee_version.storage; + let constants = &platform_version + .drive_abci + .validation_and_processing + .event_constants; let compute_fee = compute_shielded_verification_fee_v0(num_actions, platform_version)?; @@ -78,7 +80,8 @@ pub fn compute_minimum_shielded_fee_v0( .ok_or(ProtocolError::Overflow( "shielded storage per-byte rate overflow", ))?; - let storage_fee_per_action = SHIELDED_STORAGE_BYTES_PER_ACTION + let storage_fee_per_action = constants + .shielded_storage_bytes_per_action .checked_mul(per_byte_rate) .ok_or(ProtocolError::Overflow( "shielded per-action storage fee overflow", @@ -251,10 +254,11 @@ pub fn compute_shielded_identity_create_fee_v0( mod tests { use super::*; - /// The refactored `compute_minimum_shielded_fee_v0` must return the EXACT same value as the - /// historical formula `proof + num_actions × (processing + 312×rate)`. The refactor splits the - /// computation into `compute_fee + num_actions × (312×rate)`; this asserts the two are equal - /// across a range of action counts so the consensus fee is byte-for-byte unchanged. + /// `compute_minimum_shielded_fee_v0` must equal + /// `proof + num_actions × (processing + allowance×rate)` with every term read from the + /// version's own constant tables, and must decompose as + /// `compute_fee + num_actions × storage_allowance` — the component split the pool-paid + /// booking and the fee-floor tests rely on. #[test] fn compute_minimum_shielded_fee_v0_equals_historical_formula() { let platform_version = PlatformVersion::latest(); @@ -267,9 +271,9 @@ mod tests { storage.storage_disk_usage_credit_per_byte + storage.storage_processing_credit_per_byte; for num_actions in [0usize, 1, 2, 5, 16] { - // Historical: proof + num_actions × (processing + 312×rate) + // Structure: proof + num_actions × (processing + allowance×rate) let per_action = constants.shielded_per_action_processing_fee - + SHIELDED_STORAGE_BYTES_PER_ACTION * per_byte_rate; + + constants.shielded_storage_bytes_per_action * per_byte_rate; let historical = constants.shielded_proof_verification_fee + (num_actions as u64) * per_action; @@ -286,7 +290,9 @@ mod tests { assert_eq!( refactored, compute_fee - + (num_actions as u64) * SHIELDED_STORAGE_BYTES_PER_ACTION * per_byte_rate, + + (num_actions as u64) + * constants.shielded_storage_bytes_per_action + * per_byte_rate, "minimum fee must equal compute fee plus the per-action storage estimate" ); } diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index f01b04303f4..edf4a0a7de2 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -29,34 +29,6 @@ pub use sighash::{ unshield_extra_sighash_data_v0, }; -/// Permanent storage bytes per shielded action: 344 bytes total. -/// -/// - 312 bytes in the BulkAppendTree: 32 (`cmx`, the note commitment) + 32 -/// (`rho`) + 32 (`cv_net`, the value commitment, stored unencrypted for OVK -/// recovery) + 216 (the encrypted note ciphertext). -/// - 32 bytes in the nullifier tree. -/// -/// The 216-byte encrypted note is Orchard's `TransmittedNoteCiphertext`, laid -/// out as `epk(32) || enc_ciphertext(104) || out_ciphertext(80)`: -/// -/// - `epk` (32): the note's ephemeral public key, published in the clear. The -/// recipient combines it with their incoming viewing key (Diffie–Hellman) to -/// derive the AEAD key. -/// - `enc_ciphertext` (104): the note encrypted to the recipient (opened with -/// the incoming viewing key) — ChaCha20-Poly1305 over the note plaintext. It -/// holds the compact note (52 = version 1 + diversifier `d` 11 + value 8 + -/// `rseed` 32), the memo (36), and the AEAD tag (16); the 52-byte compact -/// prefix is what wallets trial-decrypt during sync to detect their own notes. -/// - `out_ciphertext` (80): the note encrypted to the sender for wallet -/// recovery (opened with the outgoing viewing key): out plaintext -/// (64 = `pk_d` 32 + `esk` 32) + AEAD tag (16). -/// -/// This is the standard Orchard layout except the memo is 36 bytes (`DashMemo`) -/// instead of Zcash's 512 — the dashpay `orchard` fork makes the memo size a -/// type parameter (`MemoSize`) — which is why each note is 216 bytes -/// (`ENCRYPTED_NOTE_SIZE`) rather than Zcash Orchard's ~692. -pub const SHIELDED_STORAGE_BYTES_PER_ACTION: u64 = 344; - /// Calibrated effective storage-byte cost of the Core withdrawal document a /// `ShieldedWithdrawal` creates. /// diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index c538ac479fd..1ad3ac1be5b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1984,11 +1984,17 @@ mod tests { /// the block without it compute a different app hash and can never agree. /// /// This pins the invariant that a dropped transition must not mutate state. + /// + /// Runs under protocol version 13 — the version mainnet was on at the halt, whose fee + /// constants the band edges were measured under. Protocol 14 rebalances the shielded + /// fee constants (40M proof verification, 550-byte storage allowance), which moves the + /// funding band; the mainnet reproduction belongs to the mainnet protocol version. #[tokio::test] async fn dropped_shield_must_not_mutate_state() { - let pv = PlatformVersion::latest(); + let pv = PlatformVersion::get(13).expect("protocol version 13 should exist"); let b = build_bundle(); - // Sits inside the measured band: accepted by validation, rejected by execution. + // Sits inside the band measured under protocol version 13: accepted by validation, + // rejected by execution. let headroom = 177_215_759u64; let mut platform = setup_platform(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_transfer/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_transfer/tests.rs index c082463778e..cac4f107600 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_transfer/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_transfer/tests.rs @@ -439,7 +439,7 @@ mod tests { // min_fee = proof_verification_fee + num_actions × (processing_fee + storage_fee) // // The exact per-action / per-bundle constants live in `dpp` and evolve across protocol versions - // (e.g. `SHIELDED_STORAGE_BYTES_PER_ACTION` changed when `cv_net` was added). Rather than + // (e.g. the storage allowance changed when `cv_net` was added). Rather than // hardcode the resulting numbers (which silently go stale when a constant changes), these tests // source the threshold from the canonical `dpp::shielded::compute_minimum_shielded_fee` via the // module-level `minimum_fee(num_actions)` helper, so the fixture fee always matches the consensus diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index b80eb387299..8daf798f772 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -47,6 +47,20 @@ pub struct DriveAbciValidationConstants { /// Per-action fee (in credits) for processing: RedPallas spend auth signature /// verification, nullifier duplicate check, and tree insertion. pub shielded_per_action_processing_fee: u64, + /// Per-action long-term storage allowance, in bytes, priced at the full + /// storage rate (disk + processing credits per byte) by + /// `compute_minimum_shielded_fee` — the flat storage component every + /// pool-paid shielded transition carries per action. + /// + /// The physical payload is 344 bytes: 312 in the BulkAppendTree — 32 + /// (`cmx`) + 32 (`rho`) + 32 (`cv_net`, stored unencrypted for OVK + /// recovery) + 216 (the `DashMemo` Orchard `TransmittedNoteCiphertext`: + /// `epk(32) || enc_ciphertext(104) || out_ciphertext(80)`) — plus 32 in + /// the nullifier tree. The allowance may exceed that to cover what the + /// metering actually charges per append under the GroveVersion in force + /// (Merk node framing, dense path records, the amortized chunk-blob + /// framing). + pub shielded_storage_bytes_per_action: u64, /// Maximum surplus (in credits) that a `ShieldFromAssetLock` may implicitly /// donate to the fee pools when no `surplus_output` address is set. Above this /// cap the transition is rejected so a client cannot accidentally forfeit a diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 6358f0f9d08..2efb3c9f0be 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -283,6 +283,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 9195558adb4..c2888e6824a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -333,11 +333,25 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = minimum_pool_notes_for_outgoing: 250, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, - shielded_proof_verification_fee: 100_000_000, + // Rebalanced for protocol 14: one Halo 2 bundle verification is + // ~5 ms; at the fee model's ~8M credits/ms of CPU this prices it + // at ~27x a BLS signature verification — reserved for compute + // alone, never for database work (the storage allowance below + // covers that independently). + shielded_proof_verification_fee: 40_000_000, // Per-action processing prices the ~1.1 ms/action Halo 2 verification CPU at the // same rate the flat fee prices the ~5 ms base (100M ≈ 4.5× this), so the fee // tracks the per-action cost and the margin stays uniform as actions grow. shielded_per_action_processing_fee: 22_000_000, + // Rebalanced for protocol 14 alongside the proof fee: the + // GROVE_V4 fixed per-append model meters a 1-action transfer at + // 17,882,707 credits total, 14,337,000 of it storage — 523 + // bytes at the full 27,400 credits/byte rate, the declared + // 344-byte payload plus Merk framing, dense path records and + // the amortized chunk framing. 550 covers that with headroom, + // so the storage component alone pays for the database work and + // the compute fees above stay reserved for compute. + shielded_storage_bytes_per_action: 550, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index 2bfc9d718ee..2b112634367 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -283,6 +283,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 7b1f614673e..127a31501a9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -283,6 +283,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 0e97b7cc9c0..b1b51f52499 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -286,6 +286,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index d8af07df7eb..0dfdcf7c649 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -287,6 +287,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 6f663a3e5e2..8e281ad561d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -290,6 +290,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index e47f2a297e9..8c067136372 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -284,6 +284,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = // Pinning every version to the same per-action fee lets a client computing the // fee under a stale protocol version still reserve the consensus-correct amount. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index b7563920b2a..42ca43abdc2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -337,6 +337,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // same rate the flat fee prices the ~5 ms base (100M ≈ 4.5× this), so the fee // tracks the per-action cost and the margin stays uniform as actions grow. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). shielded_identity_create_denominations: &[ diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index a652b10d6cc..7b16038ea7b 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -333,6 +333,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = // same rate the flat fee prices the ~5 ms base (100M ≈ 4.5× this), so the fee // tracks the per-action cost and the margin stays uniform as actions grow. shielded_per_action_processing_fee: 22_000_000, + // The declared physical payload (312 note bytes + 32 nullifier + // bytes); locked — released versions replay what they charged. + shielded_storage_bytes_per_action: 344, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. diff --git a/packages/rs-platform-wallet-ffi/src/shielded_send.rs b/packages/rs-platform-wallet-ffi/src/shielded_send.rs index 21d98fac4db..0926f6eff14 100644 --- a/packages/rs-platform-wallet-ffi/src/shielded_send.rs +++ b/packages/rs-platform-wallet-ffi/src/shielded_send.rs @@ -1656,15 +1656,15 @@ mod tests { // kind 0 — ShieldedTransfer / Shield base. assert_eq!( estimate(0), - 162_851_200, + 114_140_000, "shielded transfer fee (2 actions)" ); // kind 1 — Unshield. - assert_eq!(estimate(1), 168_934_000, "unshield fee (2 actions)"); + assert_eq!(estimate(1), 120_222_800, "unshield fee (2 actions)"); // kind 2 — ShieldedWithdrawal. assert_eq!( estimate(2), - 275_191_200, + 226_480_000, "shielded withdrawal fee (2 actions)" ); } From 77277f3ebeff919d8dc3e0f8c022bbc792de6eb0 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 24 Aug 2026 14:32:05 +0200 Subject: [PATCH 2/2] test(drive-abci): self-calibrate the halt-repro band; pin cross-version fee goldens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mainnet-halt-repro's hardcoded mid-band headroom went stale the moment the protocol-14 constants moved (and parts of execution resolve constants through the platform state's version rather than the passed one, so the pv-13 run shifted too). The test now binary-searches the protocol-13 funding band at runtime — least headroom validation lets through (successful or dropped both count as through; below the band the rejections are the structural-minimum gate, then AddressesNotEnoughFunds), least headroom that executes — asserts the band is still open, and drops a shield funded at the midpoint. Measured here: [98,296,560, 117,215,720), the same 18.9M width as the historical measurement. Also per review: an independent cross-version golden (protocols 12/13 must keep the shipped 162,851,200 two-action fee byte-for-byte, protocol 14 must price it at exactly 114,140,000 — hardcoded so a released table accidentally given the new allowance cannot pass), and the protocol-14 per-action fee comment no longer claims a shared calibration rate with the rebalanced 40M bundle fee. Co-Authored-By: Claude Fable 5 --- .../compute_minimum_shielded_fee/v0/mod.rs | 26 ++++++++ .../state_transitions/shield/tests.rs | 63 ++++++++++++++++--- .../drive_abci_validation_versions/v10.rs | 11 +++- 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index 533ba0c6182..8066bca099a 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -298,6 +298,32 @@ mod tests { } } + /// Independent boundary goldens across the protocol-14 rebalance: the released protocol-12 + /// and protocol-13 tables must keep producing the shipped 162,851,200-credit two-action fee + /// byte-for-byte (100M proof + 2 × 22M processing + 2 × 344 B × 27,400), and protocol 14 + /// must produce exactly 114,140,000 (40M proof + 2 × 22M + 2 × 550 B × 27,400). Hardcoded + /// on purpose — deriving the expectation from the same table field the implementation + /// reads would pass even if a released table were accidentally given the new allowance. + #[test] + fn minimum_shielded_fee_changes_only_at_protocol_14() { + for protocol_version in [12, 13] { + let platform_version = PlatformVersion::get(protocol_version) + .expect("released shielded protocol version should exist"); + assert_eq!( + compute_minimum_shielded_fee_v0(2, platform_version) + .expect("released minimum shielded fee"), + 162_851_200, + "protocol {protocol_version} must keep the shipped two-action fee byte-for-byte" + ); + } + let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert_eq!( + compute_minimum_shielded_fee_v0(2, platform_version).expect("minimum shielded fee"), + 114_140_000, + "protocol 14 must price a two-action bundle at the rebalanced constants" + ); + } + /// Pin the exact relationship between the ShieldedWithdrawal fee and the base shielded fee: /// the withdrawal fee MUST be `compute_minimum_shielded_fee_v0(n)` plus exactly one flat /// `SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES × per_byte_rate` document component (the same diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index 1ad3ac1be5b..d9dc23a97bb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1985,17 +1985,66 @@ mod tests { /// /// This pins the invariant that a dropped transition must not mutate state. /// - /// Runs under protocol version 13 — the version mainnet was on at the halt, whose fee - /// constants the band edges were measured under. Protocol 14 rebalances the shielded - /// fee constants (40M proof verification, 550-byte storage allowance), which moves the - /// funding band; the mainnet reproduction belongs to the mainnet protocol version. + /// Runs under protocol version 13 — the version mainnet was on at the halt, the one + /// whose estimation path leaves the funding band open (the estimated-cost model skips + /// the keyless commitment-tree append, dashpay/grovedb#812). The band's absolute edges + /// move whenever fee constants or grovedb cost models move — and parts of execution + /// resolve constants through the platform state's own version rather than the passed + /// one — so a hardcoded headroom goes stale. The test therefore calibrates itself: it + /// binary-searches the least headroom validation lets through and the least headroom + /// that executes, asserts the band between them is still open, and drops a shield + /// funded at the midpoint. #[tokio::test] async fn dropped_shield_must_not_mutate_state() { let pv = PlatformVersion::get(13).expect("protocol version 13 should exist"); let b = build_bundle(); - // Sits inside the band measured under protocol version 13: accepted by validation, - // rejected by execution. - let headroom = 177_215_759u64; + + // Least headroom validation lets through to execution. + const CEILING: u64 = 5_000_000_000; + let (top, top_msg) = run_at(CEILING, &b, pv).await; + assert_eq!( + top, + Outcome::Success, + "sanity: the ceiling must comfortably fund the shield ({top_msg})" + ); + // "Accepted" means validation let the transition through to + // execution: the outcome is either a successful execution or the + // mid-band InternalError drop. Everything below the band is a + // clean rejection — the structural-minimum gate at tiny + // headroom, then AddressesNotEnoughFunds — so the predicate is + // monotone across the funding range. + let is_accepted = + |outcome: Outcome| matches!(outcome, Outcome::Internal | Outcome::Success); + let (mut lo, mut hi) = (0u64, CEILING); + while lo + 1 < hi { + let mid = lo + (hi - lo) / 2; + if is_accepted(run_at(mid, &b, pv).await.0) { + hi = mid; + } else { + lo = mid; + } + } + let accepted = hi; + // Least headroom that actually executes. + let (mut lo, mut hi) = (accepted, CEILING); + while lo + 1 < hi { + let mid = lo + (hi - lo) / 2; + if run_at(mid, &b, pv).await.0 == Outcome::Success { + hi = mid; + } else { + lo = mid; + } + } + let executes = hi; + println!("band at protocol 13: [{accepted}, {executes})"); + assert!( + accepted < executes, + "the protocol-13 funding band must be open (its estimator skips the keyless \ + commitment-tree append); if it has closed, this reproduction is no longer \ + constructible and should be retired deliberately" + ); + // Mid-band: accepted by validation, rejected by execution. + let headroom = accepted + (executes - accepted) / 2; let mut platform = setup_platform(); insert_dummy_encrypted_notes(&platform, MAINNET_NOTES); diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index c2888e6824a..33394779fec 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -339,9 +339,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // alone, never for database work (the storage allowance below // covers that independently). shielded_proof_verification_fee: 40_000_000, - // Per-action processing prices the ~1.1 ms/action Halo 2 verification CPU at the - // same rate the flat fee prices the ~5 ms base (100M ≈ 4.5× this), so the fee - // tracks the per-action cost and the margin stays uniform as actions grow. + // Retained from protocol 13, versioned independently of the + // rebalanced bundle proof fee above: it prices the per-action + // work — the marginal Halo 2 verification CPU (~1.1 ms/action), + // the RedPallas spend-auth check, the nullifier duplicate check + // and the tree-insertion processing — and its ~18M headroom over + // the ~3.5M credits of metered per-append GroveDB processing is + // deliberate, not a shared calibration rate with the 40M bundle + // fee. shielded_per_action_processing_fee: 22_000_000, // Rebalanced for protocol 14 alongside the proof fee: the // GROVE_V4 fixed per-append model meters a 1-action transfer at