Skip to content
Open
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
65 changes: 65 additions & 0 deletions book/src/fees/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,13 +534,78 @@ pub struct FeeVersion {
pub data_contract_registration: FeeDataContractRegistrationVersion,
pub state_transition_min_fees: StateTransitionMinFees,
pub vote_resolution_fund_fees: VoteResolutionFundFees,
pub dashvm: Option<FeeDashVmVersion>,
}
```

Fee versions are stored in the `FEE_VERSIONS` array and looked up by number. The
`uses_version_fee_multiplier_permille` field allows a global scaling factor
(permille = divide by 1000; a value of 1000 means no change).

`fee_version_number` keys the persisted fee history and the storage refund
rates, so it changes whenever a group the history serves changes: the storage,
processing, hashing or signature rates. A schedule that changes only a group
the history never serves keeps the number of the generation it agrees with and
is not appended to `FEE_VERSIONS`: `FEE_VERSION2` (protocol version 9),
`FEE_VERSION3` (protocol version 14) and `FEE_VERSION4` (protocol version 17)
all carry number 1, and a test on `FEE_VERSION4` pins that it agrees with the
registered generation on every served group.

The epoch fee history (`previous_fee_versions` in platform state) records a
schedule only when its number changes, is saved as numbers and restored through
`FeeVersion::get(number)`, and serves exactly the groups `KnownCostItem` reads:
storage, processing, hashing and signature. Every other group
(`data_contract_validation`, `data_contract_registration`,
`state_transition_min_fees`, `vote_resolution_fund_fees`, `dashvm`) is read
from the active protocol version's schedule, `platform_version.fee_version`,
and never from the history.
Upgrading from protocol version 16 to 17 therefore records nothing new in the
history and a restart resolves the existing entry to `FEE_VERSION1`; contract
pricing is unaffected because nothing reads it from there.

### Smart-contract computation (protocol version 17, 5.0)

This section describes the contract the tables define. Protocol version 17
carries the limits and the price, but nothing dispatches on them yet: the
runtime that meters invocations, the block loop that reserves against the
per-block ledger and the fee pipeline that charges the units arrive with later
5.0 tasks, so a node at protocol version 17 today enforces no computation
limit and charges no computation fee.

Smart-contract work is metered by the runtime in *computation units*
(`ComputationUnits` in `rs-platform-version`): a deterministic count of the
admitted guest operations and host work an invocation performs, weighted by the
active metering generation. A unit is never wall-clock time, so every node
counts the same number of units for the same invocation, whatever its hardware
or cache state.

Two consensus limits bound the units, both in
`SystemLimits::smart_contract_computation` and both counted by one
contract-only counter that is separate from every native budget (the proposer
timer, the withdrawal and shielded per-block caps, the Tenderdash block gas
limit):

| Limit | Scope |
|---|---|
| `max_computation_units_per_invocation` | One outer invocation: a direct call, a predicate, or one scheduled attempt, including every nested call, predicate, module initialisation and host entry it causes. The runtime receives it as the budget of the invocation. |
| `max_computation_units_per_block` | All invocations in one block, ordinary and scheduled. The block loop reserves an invocation's admitted bound before it runs and settles the actual consumption afterwards (`BlockComputationBudget` in `rs-drive-abci`), so an invocation that would not fit is delayed or rejected, never failed part-way through. |

Units become credits at the active protocol version's price,
`platform_version.fee_version.dashvm.credits_per_computation_unit`, through
`dpp::fee::smart_contract_computation::computation_units_to_credits` (checked
multiplication; the function takes `&PlatformVersion`, so a schedule taken from
the epoch fee history cannot be passed to it). The charge enters the processing fee of the invocation's
`FeeResult`, which is what Tenderdash's `gas_used` and `gas_wanted` already
report, so gas stays denominated in credits and no unit equivalence between
computation units and Tenderdash gas exists. A failed invocation still consumed
its units and is charged for them.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Protocol versions before 17 carry `None` for both the limits and the price:
nothing meters, prices or budgets contract computation there. The numbers on
protocol version 17 (25 million units per invocation, 250 million per block,
1 credit per unit) are the provisional starting values of the DashVM allocation
register and are measured and revised before any network runs that version.

## Key Source Files

| File | Contents |
Expand Down
10 changes: 9 additions & 1 deletion book/src/versioning/feature-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,15 +338,23 @@ pub struct SystemLimits {
pub max_token_redemption_cycles: u32,
pub max_shielded_transition_actions: u16,
pub max_time_range_overlap_factor: Option<u64>,
// ...
pub smart_contract_computation: Option<SmartContractComputationLimits>,
}
```

There are four `SYSTEM_LIMITS_V*` constants, one for each protocol version at
There are five `SYSTEM_LIMITS_V*` constants, one for each protocol version at
which a limit changed. The `Option` fields show the idiom for a parameter that
did not exist before some version: `None` in the tables of the versions that
predate the rule, `Some(value)` from the version that introduced it. It is the
parameter-shaped twin of `OptionalFeatureVersion`.

Nested optional groups follow the same rule as optional method versions:
`smart_contract_computation` is `None` on every protocol version that predates
smart contracts and carries the per-invocation and per-block computation
limits from protocol version 17. Consumers pass the group as one value, and
later limits of the same family extend the group rather than the flat table.

The same shape recurs wherever a subsystem owns tunables:

```rust
Expand Down
43 changes: 35 additions & 8 deletions book/src/versioning/platform-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,20 @@ function version so that execution is deterministic.

## The Version Array

Each protocol version gets its own constant, defined in a separate file. At
the time of writing, the platform has fourteen versions:
Each protocol version gets its own constant, defined in a separate file. On
the 5.0 development branch the platform has seventeen versions:

```rust
// packages/rs-platform-version/src/version/mod.rs

pub type ProtocolVersion = u32;

pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14;
pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_17;
pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1;
pub const ALL_VERSIONS: RangeInclusive<ProtocolVersion> = 1..=LATEST_VERSION;
```

These fourteen snapshots are collected into a single static array in
These seventeen snapshots are collected into a single static array in
`protocol_version.rs`:

```rust
Expand All @@ -90,14 +90,17 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[
PLATFORM_V12,
PLATFORM_V13,
PLATFORM_V14,
PLATFORM_V15,
PLATFORM_V16,
PLATFORM_V17,
];

pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14;
pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V17;
pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION;
```

The array is indexed by protocol version number minus one (since versions are
1-indexed). `PLATFORM_V1` sits at index 0, `PLATFORM_V14` at index 13. This
1-indexed). `PLATFORM_V1` sits at index 0, `PLATFORM_V17` at index 16. This
simple layout is what makes the `get` function so fast.

One file, one protocol version. `v14.rs` was created when the first consensus
Expand All @@ -108,6 +111,30 @@ record, and the next consensus change creates `v15.rs`. There is never a
`v14.rs` that means one thing on a node built last month and another on a node
built today.

Because the array is indexed by number, a version cannot be registered without
every number below it. The 5.0 development branch therefore carries protocol
version 17 (its own) together with 15 and 16, which the allocation register
Comment thread
coderabbitai[bot] marked this conversation as resolved.
reserves for the 4.3 and 4.4 releases. Until those branches merge their real
`v15.rs` and `v16.rs` forward, the two files are placeholders written as
struct updates over their predecessor
(`PlatformVersion { protocol_version: PROTOCOL_VERSION_15, ..PLATFORM_V14 }`).
A forward merge that brings the real file is resolved by taking the incoming
file; because 16 and 17 are struct updates too, every table the incoming
version changes flows into them without a second edit, with one exception:
`PLATFORM_V17` overrides `fee_version` and `system_limits` with its own
generations (`FEE_VERSION4`, `SYSTEM_LIMITS_V5`), which are built on the
tables that were current when they were written, not on whatever version 16
carries. If the incoming version changes either of those two tables, rebase the
5.0 generation onto the incoming one (`..FEE_VERSION<incoming>`,
`..SYSTEM_LIMITS_V<incoming>`) and renumber it past the incoming constant. The
test that pins version 17 to differ from version 16 only in the
smart-contract computation tables fails until that is done.

`system_limits` is a public module, so `SystemLimits` and the nested limit
groups it holds (such as `SmartContractComputationLimits` and the
`ComputationUnits` alias) can be named from `dpp`, `drive-abci` and the
runtime crates.

## What a Version Snapshot Looks Like

Here is the very first version, `PLATFORM_V1`, slightly abbreviated:
Expand Down Expand Up @@ -278,8 +305,8 @@ impl PlatformVersion {
}
```

This is a simple array lookup. Protocol version 1 maps to index 0, version 14
to index 13. If the version number is out of range, you get a clear error. No
This is a simple array lookup. Protocol version 1 maps to index 0, version 17
to index 16. If the version number is out of range, you get a clear error. No
hash maps, no runtime registration, no dynamic dispatch -- just a static array
of compile-time constants.

Expand Down
1 change: 1 addition & 0 deletions packages/rs-dpp/src/fee/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ pub mod default_costs;
pub mod epoch;
#[cfg(feature = "fee-distribution")]
pub mod fee_result;
pub mod smart_contract_computation;

pub use crate::balances::credits::{Credits, SignedCredits};
149 changes: 149 additions & 0 deletions packages/rs-dpp/src/fee/smart_contract_computation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! Pricing of smart-contract computation.
//!
//! Contract work is metered by the runtime in [`ComputationUnits`], a deterministic count under
//! the active metering generation, and bounded per invocation and per block by
//! [`SmartContractComputationLimits`] in the protocol version's system limits. This module turns
//! the units an invocation consumed into credits at the protocol-versioned price of the fee
//! schedule (`FeeVersion::dashvm`).
//!
//! The price is read from the fee schedule of the **active protocol version**
//! (`platform_version.fee_version.dashvm`), never from the persisted epoch fee history. That
//! history is keyed by `fee_version_number`, records a schedule only when that number changes,
//! and is restored from saved state through `FeeVersion::get(number)`; it serves the storage,
//! processing, hashing and signature groups (`KnownCostItem`) and the storage refund rates, and
//! nothing else. A schedule that adds contract pricing does not change the number, so the history
//! never carries the `dashvm` group, exactly as it never carried `data_contract_registration`,
//! `state_transition_min_fees` or `vote_resolution_fund_fees`, all of which are likewise read from
//! the active protocol version. The function below therefore takes `&PlatformVersion`, so a
//! history entry cannot be passed to it by mistake.
//!
//! The charge enters the processing fee of the invocation's `FeeResult`, exactly like every other
//! processing charge, and therefore reaches Tenderdash through the existing `gas_used` and
//! `gas_wanted` fields, which report `FeeResult::total_base_fee()` in credits. Gas stays
//! denominated in credits; computation units are never reported to Tenderdash and no unit
//! equivalence between them and Tenderdash gas exists.

use crate::fee::Credits;
use crate::ProtocolError;
pub use platform_version::version::system_limits::smart_contract::{
ComputationUnits, SmartContractComputationLimits,
};
use platform_version::version::PlatformVersion;

/// Prices `units` of smart-contract computation in credits at the active protocol version's
/// rate (`platform_version.fee_version.dashvm.credits_per_computation_unit`).
///
/// The table is the versioned part: a schedule that prices computation differently is a new
/// `FEE_VERSION*` with a different `dashvm` group, not a new generation of this function.
/// Callers on a block path pass the version from platform state
/// (`platform_state.current_platform_version()`), never `PlatformVersion::latest()` and never a
/// schedule taken from the epoch fee history (see the module documentation).
///
/// # Errors
///
/// * `ProtocolError::CorruptedCodeExecution` when the protocol version has no smart-contract
/// pricing. A caller only reaches this function after the protocol version admitted contract
/// execution, and the tables guarantee that such a version prices computation, so a missing
/// price is a broken build rather than a user mistake.
/// * `ProtocolError::Overflow` when the charge does not fit in `Credits`. With the provisional
/// rate of one credit per unit and limits far below `u64::MAX` this is unreachable, but the
/// arithmetic is checked so that no revision of either table can wrap a fee.
pub fn computation_units_to_credits(
units: ComputationUnits,
platform_version: &PlatformVersion,
) -> Result<Credits, ProtocolError> {
let price = platform_version
.fee_version
.dashvm
.as_ref()
.ok_or_else(|| {
ProtocolError::CorruptedCodeExecution(format!(
"computation_units_to_credits requires fee_version.dashvm, which protocol version {} does not carry",
platform_version.protocol_version
))
})?;

units
.checked_mul(price.credits_per_computation_unit)
.ok_or(ProtocolError::Overflow(
"smart-contract computation charge overflowed credits",
))
}

#[cfg(test)]
mod tests {
use super::*;
use platform_version::version::fee::dashvm::FeeDashVmVersion;
use platform_version::version::fee::FeeVersion;
use platform_version::version::PlatformVersion;

#[test]
fn should_price_computation_units_at_the_schedule_rate() {
let platform_version = PlatformVersion::latest();
let limits = platform_version
.system_limits
.smart_contract_computation
.as_ref()
.expect("the latest protocol version bounds smart-contract computation");
let price = platform_version
.fee_version
.dashvm
.as_ref()
.expect("the latest protocol version prices smart-contract computation");

let units = limits.max_computation_units_per_invocation;

let credits = computation_units_to_credits(units, platform_version)
.expect("a maximal invocation must be priceable");

assert_eq!(credits, units * price.credits_per_computation_unit);
}

#[test]
fn should_price_zero_units_as_zero_credits() {
let platform_version = PlatformVersion::latest();

let credits = computation_units_to_credits(0, platform_version)
.expect("zero units must be priceable");

assert_eq!(credits, 0);
}

#[test]
fn should_fail_with_overflow_when_the_charge_does_not_fit_in_credits() {
let latest = PlatformVersion::latest();
let platform_version = PlatformVersion {
fee_version: FeeVersion {
dashvm: Some(FeeDashVmVersion {
credits_per_computation_unit: 2,
}),
..latest.fee_version.clone()
},
..latest.clone()
};

let result = computation_units_to_credits(u64::MAX, &platform_version);

assert!(
matches!(result, Err(ProtocolError::Overflow(_))),
"expected an overflow error, got {result:?}"
);
}

#[test]
fn should_report_corrupted_code_execution_when_the_protocol_version_has_no_smart_contract_pricing(
) {
let platform_version = PlatformVersion::get(14).expect("protocol version 14 exists");
assert!(
platform_version.fee_version.dashvm.is_none(),
"protocol version 14 predates smart-contract pricing"
);

let result = computation_units_to_credits(1, platform_version);

assert!(
matches!(result, Err(ProtocolError::CorruptedCodeExecution(_))),
"expected a corrupted code execution error, got {result:?}"
);
}
}
Loading
Loading