Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -69,6 +67,10 @@ pub fn compute_minimum_shielded_fee_v0(
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
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)?;

Expand All @@ -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",
Expand Down Expand Up @@ -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();
Expand All @@ -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;

Expand All @@ -286,12 +290,40 @@ 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"
);
}
Comment thread
QuantumExplorer marked this conversation as resolved.
}

/// 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
Expand Down
28 changes: 0 additions & 28 deletions packages/rs-dpp/src/shielded/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1984,12 +1984,67 @@ 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, 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::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.
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,30 @@ 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,
// 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.
// 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,
// 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
// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: &[],
},
Expand Down
Loading
Loading