diff --git a/book/src/fees/overview.md b/book/src/fees/overview.md index 94af5b6e964..44bacc98872 100644 --- a/book/src/fees/overview.md +++ b/book/src/fees/overview.md @@ -534,6 +534,7 @@ pub struct FeeVersion { pub data_contract_registration: FeeDataContractRegistrationVersion, pub state_transition_min_fees: StateTransitionMinFees, pub vote_resolution_fund_fees: VoteResolutionFundFees, + pub dashvm: Option, } ``` @@ -541,6 +542,70 @@ 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. + +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 | diff --git a/book/src/versioning/feature-versions.md b/book/src/versioning/feature-versions.md index f2395089285..13a21bcb039 100644 --- a/book/src/versioning/feature-versions.md +++ b/book/src/versioning/feature-versions.md @@ -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, + // ... + pub smart_contract_computation: Option, } ``` -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 diff --git a/book/src/versioning/platform-version.md b/book/src/versioning/platform-version.md index 32e7db207f5..065cbbbae6c 100644 --- a/book/src/versioning/platform-version.md +++ b/book/src/versioning/platform-version.md @@ -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 = 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 @@ -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 @@ -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 +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`, +`..SYSTEM_LIMITS_V`) 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: @@ -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. diff --git a/packages/rs-dpp/src/fee/mod.rs b/packages/rs-dpp/src/fee/mod.rs index f89ba6be42a..7ef1f926b4e 100644 --- a/packages/rs-dpp/src/fee/mod.rs +++ b/packages/rs-dpp/src/fee/mod.rs @@ -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}; diff --git a/packages/rs-dpp/src/fee/smart_contract_computation.rs b/packages/rs-dpp/src/fee/smart_contract_computation.rs new file mode 100644 index 00000000000..51aea5b5db5 --- /dev/null +++ b/packages/rs-dpp/src/fee/smart_contract_computation.rs @@ -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 { + 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:?}" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/upgrade_protocol_version/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/upgrade_protocol_version/v0/mod.rs index f87f67705b5..06c9395490a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/upgrade_protocol_version/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/upgrade_protocol_version/v0/mod.rs @@ -177,6 +177,11 @@ mod tests { use crate::test::helpers::setup::TestPlatformBuilder; use dpp::block::block_info::BlockInfo; use dpp::block::epoch::Epoch; + use dpp::fee::default_costs::EpochCosts; + use dpp::fee::smart_contract_computation::computation_units_to_credits; + use dpp::serialization::PlatformDeserializableFromVersionedStructureTrusted; + use dpp::version::v16::PROTOCOL_VERSION_16; + use dpp::version::v17::PROTOCOL_VERSION_17; use dpp::version::PlatformVersion; #[test] @@ -464,4 +469,133 @@ mod tests { assert!(counter.get(&platform_version.protocol_version).is_ok()); } } + + /// The 5.0 protocol version prices smart-contract computation through a fee schedule that + /// keeps `fee_version_number` 1, because no rate the epoch fee history serves changes. This + /// pins what that means end to end: upgrading 16 to 17 on an epoch change records nothing new + /// in the history, the history keeps resolving to the registered generation without contract + /// pricing, saving and reloading the upgraded state keeps it that way, and the price is + /// nevertheless available on every one of those paths through the active protocol version. + /// A schedule that reached the history would mean a storage rate changed, which is a + /// different change with its own number. + #[test] + fn test_upgrade_to_the_5_0_protocol_version_keeps_the_fee_history_and_prices_computation_from_the_active_version( + ) { + let previous_version = + PlatformVersion::get(PROTOCOL_VERSION_16).expect("protocol version 16 exists"); + let upgraded_version = + PlatformVersion::get(PROTOCOL_VERSION_17).expect("protocol version 17 exists"); + assert_eq!( + previous_version.fee_version.fee_version_number, + upgraded_version.fee_version.fee_version_number, + "the 5.0 schedule must keep the number of the generation it agrees with" + ); + + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(PROTOCOL_VERSION_16) + .build_with_mock_rpc() + .set_genesis_state(); + + let transaction = platform.drive.grove.start_transaction(); + + let epoch_info = EpochInfo::V0(EpochInfoV0 { + current_epoch_index: 3, + previous_epoch_index: Some(2), + is_epoch_change: true, + }); + + let block_info = BlockInfo { + time_ms: 1_000_000, + height: 300, + core_height: 100, + epoch: Epoch::new(3).expect("expected epoch"), + }; + + // A history as a network that ran protocol version 16 would have it: one entry, the + // registered generation, recorded at its first epoch change. + let last_committed_state = platform.state.load(); + let mut block_platform_state = last_committed_state.as_ref().clone(); + block_platform_state.previous_fee_versions_mut().clear(); + block_platform_state + .previous_fee_versions_mut() + .insert(1, previous_version.fee_version.as_static()); + + // The upgrade vote carried: this block runs at 17 while the last committed state is 16. + block_platform_state.set_current_protocol_version_in_consensus(PROTOCOL_VERSION_17); + + platform + .upgrade_protocol_version_on_epoch_change_v0( + &block_info, + &epoch_info, + &last_committed_state, + &mut block_platform_state, + &transaction, + upgraded_version, + ) + .expect("the upgrade hook must succeed on the epoch change"); + + let history = block_platform_state.previous_fee_versions(); + assert_eq!( + history.len(), + 1, + "a schedule that changes no rate the history serves records no new entry" + ); + let served = block_info.epoch.active_fee_version(history); + assert_eq!(served.fee_version_number, 1); + assert!( + served.dashvm.is_none(), + "the fee history never carries contract pricing" + ); + assert_eq!( + served.storage, upgraded_version.fee_version.storage, + "the history serves the same storage rates the upgraded version charges" + ); + + // Restart: the state goes through the saving format, which stores the history as + // numbers, and comes back. The standalone record is the one readable on its own; + // the per-entry structure keeps the masternode list and validator sets in the + // database and cannot be decoded from one record. + let saved = block_platform_state + .serialize_standalone_to_bytes() + .expect("the upgraded state serializes"); + let reloaded = PlatformState::versioned_deserialize_trusted(&saved, upgraded_version) + .expect("the upgraded state deserializes"); + + assert_eq!( + reloaded.current_protocol_version_in_consensus(), + PROTOCOL_VERSION_17 + ); + let reloaded_history = reloaded.previous_fee_versions(); + assert_eq!(reloaded_history.len(), 1); + let reloaded_served = block_info.epoch.active_fee_version(reloaded_history); + assert_eq!(reloaded_served.fee_version_number, 1); + assert!(reloaded_served.dashvm.is_none()); + + // The price is read from the active protocol version, which is what block execution + // obtains from platform state, before and after the restart alike. + let active_version = reloaded + .current_platform_version() + .expect("the reloaded state names a known protocol version"); + let price = active_version + .fee_version + .dashvm + .as_ref() + .expect("the 5.0 protocol version prices smart-contract computation"); + assert_eq!( + computation_units_to_credits(1_000, active_version) + .expect("the active version prices computation"), + 1_000 * price.credits_per_computation_unit + ); + assert_eq!( + computation_units_to_credits(1_000, active_version).ok(), + computation_units_to_credits( + 1_000, + block_platform_state + .current_platform_version() + .expect("the upgraded state names a known protocol version"), + ) + .ok(), + "the price is the same before and after the restart" + ); + } } diff --git a/packages/rs-drive-abci/src/execution/types/block_computation_budget.rs b/packages/rs-drive-abci/src/execution/types/block_computation_budget.rs new file mode 100644 index 00000000000..ffbe7a325f2 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/types/block_computation_budget.rs @@ -0,0 +1,490 @@ +//! The per-block ledger of smart-contract computation. +//! +//! `SystemLimits::smart_contract_computation` bounds how many computation units all contract +//! invocations in one block may consume together, ordinary and scheduled. This ledger is the +//! deterministic accounting both proposal creation and proposal validation run over the same +//! sequence of invocations: reserve an invocation's admitted bound before it runs, settle what +//! it actually consumed afterwards, release the rest. Reserving the bound rather than the actual +//! consumption means an invocation that would not fit is never started, so per-block exhaustion +//! is an admission outcome (the proposer delays the transition, a validator rejects the block) +//! and never a paid failure part-way through execution, and an invocation's own outcome does not +//! depend on its position in the block. +//! +//! The ledger is in-memory block state and is never serialised; it has no version wrapper. + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use dpp::fee::smart_contract_computation::ComputationUnits; +use dpp::version::PlatformVersion; +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Process-local identity of one ledger instance, so a reservation can only be settled into +/// the ledger that issued it. Never serialised and never consensus-visible: it only ties two +/// values in the same process together. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct LedgerId(u64); + +impl LedgerId { + fn next() -> Self { + static NEXT: AtomicU64 = AtomicU64::new(1); + Self(NEXT.fetch_add(1, Ordering::Relaxed)) + } +} + +/// The admitted computation bound of one contract invocation, held against the block's budget +/// until it is settled. +/// +/// Not `Clone` and consumed by [`BlockComputationBudget::settle`], so a reservation can only be +/// spent once, and bound to the ledger that issued it, so it cannot be settled into another +/// ledger (which would credit that ledger with units it never held). Dropping it without +/// settling keeps its bound held for the rest of the block; a caller that abandons an invocation +/// before it runs settles with zero consumption instead. +#[derive(Debug, PartialEq, Eq)] +#[must_use = "an unsettled reservation keeps its bound held for the rest of the block"] +pub struct ComputationReservation { + ledger: LedgerId, + bound: ComputationUnits, +} + +impl ComputationReservation { + /// The computation units held by this reservation. + pub fn bound(&self) -> ComputationUnits { + self.bound + } +} + +/// Why a reservation was refused: the block does not have `requested` units left, only +/// `remaining`. An admission outcome, not an error: the ledger is unchanged. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlockComputationBudgetExceeded { + /// The bound the invocation asked to reserve. + pub requested: ComputationUnits, + /// The units the block still had available. + pub remaining: ComputationUnits, +} + +/// The computation units one block may still hand to contract invocations, and the units it has +/// already handed out. +/// +/// Invariant after every operation: `consumed + held + remaining == limit`, where `held` is the +/// sum of the outstanding reservations this ledger issued. Every operation uses checked +/// arithmetic; the invariant keeps each intermediate value within `limit`, but the checks make +/// that proof local to each method rather than something a reader has to carry across the file. +/// A reservation issued by another ledger is refused by [`settle`](Self::settle) before any +/// counter changes, because its bound was never subtracted from this ledger's `remaining`. +/// +/// Cloning a ledger (the block execution context is `Clone`) copies the counters into a new +/// ledger with its own identity: the clone continues the accounting from the same state, but +/// reservations the original issued before the clone can only be settled into the original, +/// and reservations the clone issues only into the clone. The two never share a hold. +/// +/// The ledger has no equality: two ledgers with the same counters are still different ledgers +/// (their reservations are not interchangeable), and equality that included the identity would +/// make a ledger unequal to its own clone, which `Clone` and `Eq` together promise not to +/// happen. Compare `limit()`, `consumed()` and `remaining()` instead. +#[derive(Debug)] +pub struct BlockComputationBudget { + id: LedgerId, + limit: ComputationUnits, + /// Units settled as actually consumed. + consumed: ComputationUnits, + /// Units still available to reserve: the limit minus consumed and held units. + remaining: ComputationUnits, +} + +impl Clone for BlockComputationBudget { + /// A new ledger with the same counters and its own identity; see the type documentation. + fn clone(&self) -> Self { + Self { + id: LedgerId::next(), + limit: self.limit, + consumed: self.consumed, + remaining: self.remaining, + } + } +} + +impl BlockComputationBudget { + /// The ledger for a block executed under `platform_version`, or `None` when the version + /// predates smart contracts so that callers skip the contract path entirely. + pub fn for_platform_version(platform_version: &PlatformVersion) -> Option { + platform_version + .system_limits + .smart_contract_computation + .as_ref() + .map(|limits| Self::with_limit(limits.max_computation_units_per_block)) + } + + /// A ledger over an explicit limit. + pub fn with_limit(limit: ComputationUnits) -> Self { + Self { + id: LedgerId::next(), + limit, + consumed: 0, + remaining: limit, + } + } + + /// The per-block limit this ledger enforces. + pub fn limit(&self) -> ComputationUnits { + self.limit + } + + /// Units settled as actually consumed so far. + pub fn consumed(&self) -> ComputationUnits { + self.consumed + } + + /// Units still available to reserve. Outstanding reservations are not available. + pub fn remaining(&self) -> ComputationUnits { + self.remaining + } + + /// Holds `bound` units for one invocation. + /// + /// Refused, with the ledger unchanged, when fewer than `bound` units remain. Reserving zero + /// units is allowed and holds nothing. + pub fn reserve( + &mut self, + bound: ComputationUnits, + ) -> Result { + match self.remaining.checked_sub(bound) { + Some(remaining) => { + self.remaining = remaining; + Ok(ComputationReservation { + ledger: self.id, + bound, + }) + } + None => Err(BlockComputationBudgetExceeded { + requested: bound, + remaining: self.remaining, + }), + } + } + + /// Settles a reservation with the units the invocation actually consumed: the consumed + /// units are recorded and the unused part of the bound is returned to the block. Returns + /// the released units. + /// + /// Two misuses are `ExecutionError::CorruptedCodeExecution`, and the ledger is unchanged in + /// both: a reservation issued by another ledger (its bound was never held here, so releasing + /// it would let this block admit more than its limit), and `actual` above the reserved bound + /// (the runtime is handed the bound as its budget and cannot legally exceed it, so this is a + /// broken runtime, not an admission outcome). + pub fn settle( + &mut self, + reservation: ComputationReservation, + actual: ComputationUnits, + ) -> Result { + if reservation.ledger != self.id { + return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "a computation reservation was settled into a ledger other than the one that issued it", + ))); + } + + let released = reservation + .bound + .checked_sub(actual) + .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "a contract invocation consumed more computation than its reserved bound", + )))?; + + let consumed = + self.consumed + .checked_add(actual) + .ok_or(Error::Execution(ExecutionError::Overflow( + "consumed smart-contract computation overflowed the block ledger", + )))?; + let remaining = self + .remaining + .checked_add(released) + .ok_or(Error::Execution(ExecutionError::Overflow( + "released smart-contract computation overflowed the block ledger", + )))?; + + self.consumed = consumed; + self.remaining = remaining; + + Ok(released) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::version::PlatformVersion; + + fn ledger(limit: ComputationUnits) -> BlockComputationBudget { + BlockComputationBudget::with_limit(limit) + } + + #[test] + fn should_not_exist_before_the_5_0_protocol_version() { + let platform_version_14 = PlatformVersion::get(14).expect("protocol version 14 exists"); + assert!( + BlockComputationBudget::for_platform_version(platform_version_14).is_none(), + "protocol version 14 predates smart contracts and must have no computation ledger" + ); + + let latest = PlatformVersion::latest(); + let limit = latest + .system_limits + .smart_contract_computation + .as_ref() + .expect("the latest protocol version bounds smart-contract computation") + .max_computation_units_per_block; + let budget = BlockComputationBudget::for_platform_version(latest) + .expect("the latest protocol version must have a computation ledger"); + + assert_eq!(budget.limit(), limit); + assert_eq!(budget.remaining(), limit); + assert_eq!(budget.consumed(), 0); + } + + #[test] + fn should_reserve_up_to_the_exact_remaining_budget() { + let mut budget = ledger(1_000); + + let exact = budget + .reserve(1_000) + .expect("a bound equal to the remaining budget must fit"); + assert_eq!(exact.bound(), 1_000); + assert_eq!(budget.remaining(), 0); + + let refused = budget + .reserve(1) + .expect_err("one unit more than the remaining budget must be refused"); + assert_eq!( + refused, + BlockComputationBudgetExceeded { + requested: 1, + remaining: 0, + } + ); + assert_eq!( + (budget.limit(), budget.consumed(), budget.remaining()), + (1_000, 0, 0), + "a refused reservation must leave the ledger unchanged" + ); + + let released = budget + .settle(exact, 1_000) + .expect("consuming the whole bound is legal"); + assert_eq!(released, 0); + assert_eq!(budget.consumed(), 1_000); + assert_eq!(budget.remaining(), 0); + } + + #[test] + fn should_settle_actual_consumption_and_release_the_unused_bound() { + let mut budget = ledger(10_000); + + let reservation = budget.reserve(1_000).expect("1,000 units fit in 10,000"); + assert_eq!(budget.remaining(), 9_000); + + let released = budget + .settle(reservation, 600) + .expect("consuming less than the bound is legal"); + + assert_eq!(released, 400); + assert_eq!(budget.consumed(), 600); + assert_eq!(budget.remaining(), 9_400); + } + + #[test] + fn should_reject_settling_more_than_the_reserved_bound() { + let mut budget = ledger(10_000); + let reservation = budget.reserve(1_000).expect("1,000 units fit in 10,000"); + + let result = budget.settle(reservation, 1_001); + + assert!( + matches!( + result, + Err(Error::Execution(ExecutionError::CorruptedCodeExecution(_))) + ), + "expected a corrupted code execution error, got {result:?}" + ); + assert_eq!( + (budget.limit(), budget.consumed(), budget.remaining()), + (10_000, 0, 9_000), + "a rejected settlement must leave the ledger unchanged, with the bound still held" + ); + } + + #[test] + fn should_reject_settling_a_reservation_issued_by_another_ledger() { + let mut issuing = ledger(100); + let mut other = ledger(100); + + let reservation = issuing.reserve(80).expect("80 units fit in 100"); + assert_eq!(issuing.remaining(), 20); + assert_eq!(other.remaining(), 100); + + let result = other.settle(reservation, 0); + + assert!( + matches!( + result, + Err(Error::Execution(ExecutionError::CorruptedCodeExecution(_))) + ), + "expected a corrupted code execution error, got {result:?}" + ); + assert_eq!( + (other.consumed(), other.remaining()), + (0, 100), + "a foreign reservation must not credit the receiving ledger" + ); + assert_eq!( + (issuing.consumed(), issuing.remaining()), + (0, 20), + "the issuing ledger keeps the bound held; the reservation was consumed by the \ + rejected settlement and cannot be returned" + ); + let refused = other + .reserve(101) + .expect_err("the other ledger must still be bounded by its own limit"); + assert_eq!(refused.remaining, 100); + } + + #[test] + fn should_give_a_clone_its_own_identity_with_the_same_counters() { + let mut original = ledger(1_000); + let before_clone = original.reserve(300).expect("300 units fit in 1,000"); + + let mut cloned = original.clone(); + assert_eq!( + (cloned.limit(), cloned.consumed(), cloned.remaining()), + (1_000, 0, 700), + "a clone continues from the same counters" + ); + assert_ne!(original.id, cloned.id, "a clone is a distinct ledger"); + + // A reservation issued before the clone belongs to the original only. + let result = cloned.settle(before_clone, 100); + assert!( + matches!( + result, + Err(Error::Execution(ExecutionError::CorruptedCodeExecution(_))) + ), + "expected a corrupted code execution error, got {result:?}" + ); + assert_eq!((cloned.consumed(), cloned.remaining()), (0, 700)); + + // Each ledger settles what it issued, and neither sees the other's settlement. + let from_original = original.reserve(200).expect("200 units fit in 700"); + let released = original + .settle(from_original, 50) + .expect("the original settles its own reservation"); + assert_eq!(released, 150); + assert_eq!((original.consumed(), original.remaining()), (50, 650)); + + let from_clone = cloned.reserve(700).expect("700 units fit in the clone"); + let released = cloned + .settle(from_clone, 700) + .expect("the clone settles its own reservation"); + assert_eq!(released, 0); + assert_eq!((cloned.consumed(), cloned.remaining()), (700, 0)); + assert_eq!( + (original.consumed(), original.remaining()), + (50, 650), + "the clone's settlement does not touch the original" + ); + } + + #[test] + fn should_account_many_reservations_without_double_spending() { + let limit = 1_000; + let mut budget = ledger(limit); + let mut expected_consumed = 0; + + for (bound, actual) in [(300, 300), (300, 100), (200, 0), (250, 250), (150, 50)] { + let held_before = limit - budget.consumed() - budget.remaining(); + assert_eq!( + held_before, 0, + "nothing is held between settled invocations" + ); + + let reservation = budget.reserve(bound).expect("each bound fits"); + assert_eq!( + budget.consumed() + reservation.bound() + budget.remaining(), + limit, + "consumed + held + remaining must equal the limit while a reservation is out" + ); + + let released = budget.settle(reservation, actual).expect("actual <= bound"); + expected_consumed += actual; + + assert_eq!(released, bound - actual); + assert_eq!(budget.consumed(), expected_consumed); + assert_eq!( + budget.consumed() + budget.remaining(), + limit, + "consumed + remaining must equal the limit after settlement" + ); + } + + // 300 + 100 + 0 + 250 + 50 = 700 consumed; 300 units remain, so a 301-unit bound is + // refused even though the sum of the bounds reserved so far (1,200) exceeded the limit: + // released units are available again, spent units are not. + assert_eq!(budget.consumed(), 700); + assert_eq!(budget.remaining(), 300); + let refused = budget + .reserve(301) + .expect_err("301 units exceed the 300 remaining"); + assert_eq!(refused.remaining, 300); + // A reservation moved into `settle` cannot be settled again: the type system enforces + // it, which is why `ComputationReservation` is neither `Clone` nor `Copy`. + } + + #[test] + fn should_keep_the_budget_of_an_unsettled_reservation_held() { + let mut budget = ledger(1_000); + + let outstanding = budget.reserve(600).expect("600 units fit in 1,000"); + assert_eq!(budget.remaining(), 400); + + let refused = budget + .reserve(401) + .expect_err("an outstanding reservation keeps its bound unavailable"); + assert_eq!(refused.remaining, 400); + + // Abandoning an invocation before it runs is settled with zero consumption, which + // returns the whole bound; dropping the reservation instead would keep the 600 held. + let released = budget + .settle(outstanding, 0) + .expect("zero consumption is legal"); + assert_eq!(released, 600); + assert_eq!(budget.consumed(), 0); + assert_eq!(budget.remaining(), 1_000); + } + + #[test] + fn should_use_checked_arithmetic_at_the_top_of_the_range() { + let mut budget = ledger(ComputationUnits::MAX); + + let whole = budget + .reserve(ComputationUnits::MAX) + .expect("the whole budget can be reserved at once"); + assert_eq!(budget.remaining(), 0); + let refused = budget + .reserve(1) + .expect_err("nothing remains once the whole budget is held"); + assert_eq!(refused.remaining, 0); + + let released = budget + .settle(whole, ComputationUnits::MAX) + .expect("consuming the whole budget is legal"); + assert_eq!(released, 0); + assert_eq!(budget.consumed(), ComputationUnits::MAX); + assert_eq!(budget.remaining(), 0); + + // Reserving zero still succeeds with nothing left, and settling it changes nothing. + let empty = budget.reserve(0).expect("a zero bound always fits"); + let released = budget.settle(empty, 0).expect("zero consumption is legal"); + assert_eq!(released, 0); + assert_eq!(budget.consumed(), ComputationUnits::MAX); + assert_eq!(budget.remaining(), 0); + } +} diff --git a/packages/rs-drive-abci/src/execution/types/mod.rs b/packages/rs-drive-abci/src/execution/types/mod.rs index 7bc018955de..2bb3fe1b990 100644 --- a/packages/rs-drive-abci/src/execution/types/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/mod.rs @@ -1,3 +1,5 @@ +/// The per-block ledger of smart-contract computation +pub mod block_computation_budget; /// The block execution context pub mod block_execution_context; /// A structure representing block fees diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 4e4c8f0a377..ef91a14ff7e 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "latest_protocol_version": 14, + "latest_protocol_version": 17, "element_kinds": [ { "name": "Item", @@ -5206,7 +5206,7 @@ }, "layer_shapes": { "contract_groups": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "00", "right": { @@ -5215,7 +5215,7 @@ } }, "contract_groups.groups.group": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "02", "left": { @@ -5230,7 +5230,7 @@ } }, "contract_groups.members.contract": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "01", "left": { @@ -5242,7 +5242,7 @@ } }, "contracts.contract": { - "origin": "fixture contracts_with_documents@14", + "origin": "fixture contracts_with_documents@17", "tree": { "hex": "01", "left": { @@ -5254,7 +5254,7 @@ } }, "contracts.contract.other": { - "origin": "fixture moderated_contract@14", + "origin": "fixture moderated_contract@17", "tree": { "hex": "80", "left": { @@ -5272,7 +5272,7 @@ } }, "group_actions.contract.group": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "4d", "left": { @@ -5284,7 +5284,7 @@ } }, "group_actions.contract.group.active.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "53", "left": { @@ -5293,7 +5293,7 @@ } }, "group_actions.contract.group.closed.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@17", "tree": { "hex": "53", "left": { @@ -5302,7 +5302,7 @@ } }, "identities.identity": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "80", "left": { @@ -5327,7 +5327,7 @@ "states": [ { "state": "created", - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "80", "left": { @@ -5346,7 +5346,7 @@ }, { "state": "used_with_a_contract", - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "80", "left": { @@ -5368,7 +5368,7 @@ }, { "state": "budgeted_key_and_contract", - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "80", "left": { @@ -5394,7 +5394,7 @@ ] }, "identities.identity.contract_info.bound": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@17", "tree": { "hex": "01", "left": { @@ -5403,7 +5403,7 @@ } }, "identities.identity.key_references": { - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "03", "left": { @@ -5415,7 +5415,7 @@ } }, "identities.identity.key_references.authentication": { - "origin": "fixture identities@14", + "origin": "fixture identities@17", "tree": { "hex": "02", "left": { @@ -5430,7 +5430,7 @@ } }, "misc": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "45", "left": { @@ -5442,7 +5442,7 @@ } }, "pools.epoch": { - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "6d", "left": { @@ -5470,14 +5470,14 @@ "states": [ { "state": "future", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "73" } }, { "state": "running", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@17", "tree": { "hex": "6d", "left": { @@ -5505,7 +5505,7 @@ }, { "state": "paid", - "origin": "fixture paid_epoch@14", + "origin": "fixture paid_epoch@17", "tree": { "hex": "74", "left": { @@ -5528,7 +5528,7 @@ ] }, "prefunded_balances": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5540,7 +5540,7 @@ } }, "root": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "40", "left": { @@ -5597,7 +5597,7 @@ } }, "saved_block_transactions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "65", "left": { @@ -5609,7 +5609,7 @@ } }, "shielded_balances.main_pool": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5627,7 +5627,7 @@ } }, "tokens": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5648,7 +5648,7 @@ } }, "tokens.distributions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5663,7 +5663,7 @@ } }, "tokens.distributions.perpetual.token": { - "origin": "fixture token_distributions_unclaimed@14", + "origin": "fixture token_distributions_unclaimed@17", "tree": { "hex": "c0", "left": { @@ -5672,7 +5672,7 @@ } }, "tokens.distributions.timed": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "80", "left": { @@ -5684,7 +5684,7 @@ } }, "versions": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "01", "left": { @@ -5693,7 +5693,7 @@ } }, "votes": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "64", "left": { @@ -5705,7 +5705,7 @@ } }, "votes.contested_resource": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "70", "left": { @@ -5714,7 +5714,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@17", "tree": { "hex": "01", "left": { @@ -5723,7 +5723,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type.indexes.value.contender": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@17", "tree": { "hex": "01", "left": { @@ -5732,7 +5732,7 @@ } }, "withdrawals": { - "origin": "genesis@14", + "origin": "genesis@17", "tree": { "hex": "03", "left": { diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 5102d8c3e41..fe9c574f626 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -119,7 +119,7 @@ fn should_record_a_contract_layer_with_its_documents_on_top() { // fixture. Documents are read most and sit at the root of the layer; the // contract itself and everything else hang below. let contract = &json["layer_shapes"]["contracts.contract"]; - assert_eq!(contract["origin"], "fixture contracts_with_documents@14"); + assert_eq!(contract["origin"], "fixture contracts_with_documents@17"); assert_eq!(contract["tree"]["hex"], "01"); assert_eq!(contract["tree"]["left"]["hex"], "00"); assert_eq!(contract["tree"]["right"]["hex"], "02"); diff --git a/packages/rs-platform-version/src/version/fee/dashvm/mod.rs b/packages/rs-platform-version/src/version/fee/dashvm/mod.rs new file mode 100644 index 00000000000..a62cb25d58d --- /dev/null +++ b/packages/rs-platform-version/src/version/fee/dashvm/mod.rs @@ -0,0 +1,49 @@ +use bincode::{Decode, Encode}; + +pub mod v1; + +/// The prices of smart-contract work. +/// +/// Absent (`FeeVersion::dashvm == None`) on every schedule that predates smart contracts, so a +/// schedule without contract pricing cannot price computation at zero by accident: pricing a +/// unit at zero is never intended, unlike the genuinely free fees elsewhere in the tables. +/// +/// One row today, the price of a computation unit. The remaining register rows (deployment +/// validation, readiness verification, host entry, per-byte copy) are added to this group and +/// its `FEE_DASHVM_VERSION*` constant in place while the protocol version that introduces it is +/// unreleased. +/// +/// Consumers read this group from the active protocol version +/// (`platform_version.fee_version.dashvm`), never from the persisted epoch fee history or any +/// other lookup by `fee_version_number`: those resolve to the registered fee-history generation, +/// which serves only the storage, processing, hashing and signature groups and carries no +/// contract pricing. `dpp::fee::smart_contract_computation::computation_units_to_credits` takes +/// `&PlatformVersion` for that reason. +#[derive(Clone, Debug, Encode, Decode, Default, PartialEq, Eq)] +pub struct FeeDashVmVersion { + /// Processing credits charged per computation unit consumed by a contract invocation. + /// The charge is `units * credits_per_computation_unit` with checked arithmetic + /// (`dpp::fee::smart_contract_computation::computation_units_to_credits`) and enters the + /// processing fee of the invocation, which is what Tenderdash's gas fields report. + pub credits_per_computation_unit: u64, +} + +#[cfg(test)] +mod tests { + use super::FeeDashVmVersion; + + #[test] + // If this test failed, then a new field was added in FeeDashVmVersion. And the corresponding eq needs to be updated as well + fn test_fee_dashvm_version_equality() { + let version1 = FeeDashVmVersion { + credits_per_computation_unit: 1, + }; + + let version2 = FeeDashVmVersion { + credits_per_computation_unit: 1, + }; + + // This assertion will check if all fields are considered in the equality comparison + assert_eq!(version1, version2, "FeeDashVmVersion equality test failed. If a field was added or removed, update the Eq implementation."); + } +} diff --git a/packages/rs-platform-version/src/version/fee/dashvm/v1.rs b/packages/rs-platform-version/src/version/fee/dashvm/v1.rs new file mode 100644 index 00000000000..7d5302c4fe4 --- /dev/null +++ b/packages/rs-platform-version/src/version/fee/dashvm/v1.rs @@ -0,0 +1,8 @@ +use crate::version::fee::dashvm::FeeDashVmVersion; + +/// Introduced with protocol version 17 (5.0). +pub const FEE_DASHVM_VERSION1: FeeDashVmVersion = FeeDashVmVersion { + // provisional: allocation register "Provisional price", 1 processing credit per computation + // unit; measured and revised before activation + credits_per_computation_unit: 1, +}; diff --git a/packages/rs-platform-version/src/version/fee/mod.rs b/packages/rs-platform-version/src/version/fee/mod.rs index abe1f78a003..ae01fa37977 100644 --- a/packages/rs-platform-version/src/version/fee/mod.rs +++ b/packages/rs-platform-version/src/version/fee/mod.rs @@ -1,4 +1,5 @@ use crate::error::PlatformVersionError; +use crate::version::fee::dashvm::FeeDashVmVersion; use crate::version::fee::data_contract_registration::v1::FEE_DATA_CONTRACT_REGISTRATION_VERSION1; use crate::version::fee::data_contract_registration::FeeDataContractRegistrationVersion; use crate::version::fee::data_contract_validation::FeeDataContractValidationVersion; @@ -20,6 +21,7 @@ use crate::version::fee::vote_resolution_fund_fees::{ }; use bincode::{Decode, Encode}; +pub mod dashvm; pub mod data_contract_registration; mod data_contract_validation; pub mod document_ttl; @@ -31,6 +33,7 @@ pub mod storage; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; pub mod vote_resolution_fund_fees; pub type FeeVersionNumber = u32; @@ -54,6 +57,16 @@ pub struct FeeVersion { /// version 14). Platform states store only `fee_version_number`, so this field does /// not touch any stored format; `FeeVersionFieldsBeforeVersion4` must not gain it. pub document_ttl: FeeDocumentTtlVersion, + /// Prices of smart-contract work; `None` on every schedule that predates smart contracts. + /// Like `document_ttl`, never part of a stored format: `FeeVersionFieldsBeforeVersion4` + /// must not gain it. + /// + /// Read from the active protocol version's schedule (`platform_version.fee_version.dashvm`), + /// never from the persisted epoch fee history: the history is keyed by `fee_version_number`, + /// which this group does not change, and serves only the storage, processing, hashing and + /// signature groups, so a schedule looked up by number (`FeeVersion::get`, `as_static`, the + /// epoch history) carries no contract pricing. See `dpp::fee::smart_contract_computation`. + pub dashvm: Option, } impl FeeVersion { @@ -155,6 +168,8 @@ impl From for FeeVersion { vote_resolution_fund_fees: value.vote_resolution_fund_fees.into(), // Pre-4.2 tables predate the document `ttl` keyword; the group is unread there. document_ttl: FEE_DOCUMENT_TTL_VERSION1, + // Pre-4.2 tables predate smart contracts; no schedule before 5.0 prices them. + dashvm: None, } } } diff --git a/packages/rs-platform-version/src/version/fee/v1.rs b/packages/rs-platform-version/src/version/fee/v1.rs index 005c3a10c86..a25ab0e7cb8 100644 --- a/packages/rs-platform-version/src/version/fee/v1.rs +++ b/packages/rs-platform-version/src/version/fee/v1.rs @@ -22,4 +22,5 @@ pub const FEE_VERSION1: FeeVersion = FeeVersion { vote_resolution_fund_fees: VOTE_RESOLUTION_FUND_FEES_VERSION1, // Unread before protocol version 14: the document `ttl` keyword does not parse there. document_ttl: FEE_DOCUMENT_TTL_VERSION1, + dashvm: None, // smart-contract pricing arrives with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/fee/v2.rs b/packages/rs-platform-version/src/version/fee/v2.rs index 8f3f79fe7b7..4376930cc75 100644 --- a/packages/rs-platform-version/src/version/fee/v2.rs +++ b/packages/rs-platform-version/src/version/fee/v2.rs @@ -23,4 +23,5 @@ pub const FEE_VERSION2: FeeVersion = FeeVersion { vote_resolution_fund_fees: VOTE_RESOLUTION_FUND_FEES_VERSION1, // Unread before protocol version 14: the document `ttl` keyword does not parse there. document_ttl: FEE_DOCUMENT_TTL_VERSION1, + dashvm: None, // smart-contract pricing arrives with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/fee/v4.rs b/packages/rs-platform-version/src/version/fee/v4.rs new file mode 100644 index 00000000000..5402b01312f --- /dev/null +++ b/packages/rs-platform-version/src/version/fee/v4.rs @@ -0,0 +1,53 @@ +use crate::version::fee::dashvm::v1::FEE_DASHVM_VERSION1; +use crate::version::fee::v3::FEE_VERSION3; +use crate::version::fee::FeeVersion; + +/// Introduced in protocol version 17 (5.0). +/// +/// Identical to [`FEE_VERSION3`] except that it prices smart-contract computation +/// (`dashvm: Some(FEE_DASHVM_VERSION1)`). Storage, processing, hashing and signature rates are +/// unchanged, so `fee_version_number` stays 1: the number keys the persisted fee history and +/// the storage refund rates, and a schedule that only changes a group the history never serves +/// keeps the number of the generation it agrees with. For the same reason this schedule is not +/// appended to `FEE_VERSIONS`, which holds one entry per number. +/// +/// Two consequences follow, both intended. The epoch-change hook records a schedule in the fee +/// history only when its number changes, so a network upgrading to protocol version 17 keeps its +/// existing history entry, and a node restoring saved state resolves that entry to +/// [`FEE_VERSION1`](super::v1::FEE_VERSION1) through `FeeVersion::get(1)`. Neither carries the +/// `dashvm` group, and neither is meant to: contract pricing is read from the active protocol +/// version's schedule, like every other group the history does not serve. Registering this +/// schedule under a new number would not add the price to the history; it would switch every +/// storage refund at protocol version 17 onto the epoch-history refund path +/// (`fee_version_number != 1` in Drive's fee calculation) for rates that did not change. +pub const FEE_VERSION4: FeeVersion = FeeVersion { + dashvm: Some(FEE_DASHVM_VERSION1), + ..FEE_VERSION3 +}; + +#[cfg(test)] +mod tests { + use super::FEE_VERSION4; + use crate::version::fee::FeeVersion; + + /// The schedule shares its number with a registered fee-history generation, so the epoch + /// history and saved state resolve that number to the registered entry, not to this + /// schedule. That is only sound while the two agree on every group the history serves. A + /// change to a storage, processing, hashing or signature rate in this schedule needs a new + /// registered number, and this test is what says so. + #[test] + fn should_agree_with_its_registered_fee_history_generation_on_every_group_the_history_serves() { + let registered = FeeVersion::get(FEE_VERSION4.fee_version_number) + .expect("the schedule's number is registered"); + + assert_eq!(registered.storage, FEE_VERSION4.storage); + assert_eq!(registered.processing, FEE_VERSION4.processing); + assert_eq!(registered.hashing, FEE_VERSION4.hashing); + assert_eq!(registered.signature, FEE_VERSION4.signature); + + // The history never carries contract pricing; consumers read it from the active + // protocol version instead (see `dpp::fee::smart_contract_computation`). + assert!(registered.dashvm.is_none()); + assert!(FEE_VERSION4.dashvm.is_some()); + } +} diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 0ce27215cc1..88955147d6b 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -615,6 +615,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_document_expirations_per_block: 0, max_document_expiration_weight_per_block: 0, minimum_grovedb_proof_envelope_version: 0, + smart_contract_computation: None, }, consensus: ConsensusVersions { tenderdash_consensus_version: 0, diff --git a/packages/rs-platform-version/src/version/mod.rs b/packages/rs-platform-version/src/version/mod.rs index 1b1635efb42..021fcb0f97d 100644 --- a/packages/rs-platform-version/src/version/mod.rs +++ b/packages/rs-platform-version/src/version/mod.rs @@ -1,6 +1,6 @@ mod protocol_version; -use crate::version::v14::PROTOCOL_VERSION_14; +use crate::version::v17::PROTOCOL_VERSION_17; pub use protocol_version::*; use std::ops::RangeInclusive; @@ -13,13 +13,16 @@ pub mod fee; #[cfg(feature = "mock-versions")] pub mod mocks; pub mod system_data_contract_versions; -mod system_limits; +pub mod system_limits; pub mod v1; pub mod v10; pub mod v11; pub mod v12; pub mod v13; pub mod v14; +pub mod v15; +pub mod v16; +pub mod v17; pub mod v2; pub mod v3; pub mod v4; @@ -33,5 +36,5 @@ pub type ProtocolVersion = u32; pub const ALL_VERSIONS: RangeInclusive = 1..=LATEST_VERSION; -pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14; +pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_17; pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1; diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 00cc470bbc7..96236d1b0a5 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -22,6 +22,9 @@ use crate::version::v11::PLATFORM_V11; use crate::version::v12::PLATFORM_V12; use crate::version::v13::PLATFORM_V13; use crate::version::v14::PLATFORM_V14; +use crate::version::v15::PLATFORM_V15; +use crate::version::v16::PLATFORM_V16; +use crate::version::v17::PLATFORM_V17; use crate::version::v2::PLATFORM_V2; use crate::version::v3::PLATFORM_V3; use crate::version::v4::PLATFORM_V4; @@ -61,6 +64,9 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[ PLATFORM_V12, PLATFORM_V13, PLATFORM_V14, + PLATFORM_V15, + PLATFORM_V16, + PLATFORM_V17, ]; #[cfg(feature = "mock-versions")] @@ -69,7 +75,7 @@ pub static PLATFORM_TEST_VERSIONS: OnceLock> = OnceLock::ne #[cfg(feature = "mock-versions")] const DEFAULT_PLATFORM_TEST_VERSIONS: &[PlatformVersion] = &[TEST_PLATFORM_V2, TEST_PLATFORM_V3]; -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; diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 0567a7c2f6f..efd35847272 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -1,9 +1,13 @@ +pub mod smart_contract; pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; -#[derive(Clone, Debug, Default)] +use crate::version::system_limits::smart_contract::SmartContractComputationLimits; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct SystemLimits { pub estimated_contract_max_serialized_size: u16, pub max_field_value_size: u32, @@ -315,11 +319,27 @@ pub struct SystemLimits { /// version 3 (protocol version 13), so every live network already serves /// V1 by the time the floor applies. pub minimum_grovedb_proof_envelope_version: u32, + /// The consensus limits on smart-contract computation, counted in computation units by + /// one contract-only counter: how much one outer invocation may consume and how much all + /// invocations in a block may consume together. Read by the per-block computation ledger + /// in `drive-abci` (`BlockComputationBudget`) and handed to the runtime as the budget of + /// each invocation; the fee schedule's `dashvm` group prices the units. Independent of + /// every native budget (proposer timer, withdrawal and shielded per-block caps, Tenderdash + /// block gas), none of which changes. + /// + /// `None` for the protocol versions that predate smart contracts: those versions meter, + /// price and budget nothing, and a code path that reads `None` skips the contract path + /// entirely. See `SmartContractComputationLimits`. + pub smart_contract_computation: Option, } #[cfg(test)] mod tests { + use crate::version::fee::FeeVersion; use crate::version::protocol_version::PLATFORM_VERSIONS; + use crate::version::system_limits::SystemLimits; + use crate::version::v16::PLATFORM_V16; + use crate::version::v17::{PLATFORM_V17, PROTOCOL_VERSION_17}; use crate::version::{PlatformVersion, LATEST_VERSION}; /// The cap is what keeps two document operations out of a shared GroveDB batch, and with @@ -392,6 +412,133 @@ mod tests { } } + /// The computation limits and the computation price are two tables that only make sense + /// together: limits without a price would let an activated version meter contract work for + /// free, a price without limits would leave the per-block ledger nothing to reserve against + /// and the runtime no budget, so every invocation would be refused. This pins the + /// cross-table invariant on every registered version rather than restating either literal: + /// both `Some` or both `None`, the limits enforceable, the price non-zero, and nothing + /// before the 5.0 protocol version carrying either. + #[test] + fn smart_contract_computation_limits_and_pricing_activate_together() { + assert_eq!( + PLATFORM_VERSIONS.len(), + LATEST_VERSION as usize, + "the protocol version registry does not hold every declared version" + ); + for platform_version in PLATFORM_VERSIONS { + let limits = platform_version + .system_limits + .smart_contract_computation + .as_ref(); + let price = platform_version.fee_version.dashvm.as_ref(); + assert_eq!( + limits.is_some(), + price.is_some(), + "protocol version {} carries smart-contract computation limits without a price \ + or a price without limits", + platform_version.protocol_version + ); + if platform_version.protocol_version < PROTOCOL_VERSION_17 { + assert!( + limits.is_none(), + "protocol version {} predates smart contracts and must not bound their \ + computation", + platform_version.protocol_version + ); + } + if let Some(limits) = limits { + assert!( + limits.is_well_formed(), + "protocol version {} has smart-contract computation limits that cannot be \ + enforced: {limits:?}", + platform_version.protocol_version + ); + } + if let Some(price) = price { + assert_ne!( + price.credits_per_computation_unit, 0, + "protocol version {} prices smart-contract computation at zero", + platform_version.protocol_version + ); + } + } + } + + /// The 5.0 protocol version is a struct update over its predecessor that overrides exactly + /// two tables, and the placeholders for 4.3 and 4.4 are struct updates too, so a forward + /// merge that brings a real v15 or v16 must flow into v17 without a second edit. This pins + /// the delta v17 adds rather than the tables themselves: it fails when v16 gains a limit or + /// fee group that v17 does not inherit, or when a merge keeps this branch's generation over + /// an incoming one. A later 5.0 change that widens the delta extends the expected delta here + /// as part of its own change. + #[test] + fn the_5_0_protocol_version_changes_only_the_smart_contract_computation_tables() { + assert!( + PLATFORM_V17 + .system_limits + .smart_contract_computation + .is_some(), + "the 5.0 protocol version must bound smart-contract computation" + ); + assert!( + PLATFORM_V17.fee_version.dashvm.is_some(), + "the 5.0 protocol version must price smart-contract computation" + ); + assert_eq!( + SystemLimits { + smart_contract_computation: None, + ..PLATFORM_V17.system_limits.clone() + }, + PLATFORM_V16.system_limits, + "the 5.0 protocol version changes a system limit other than the smart-contract \ + computation limits without inheriting it from v16" + ); + assert_eq!( + FeeVersion { + dashvm: None, + ..PLATFORM_V17.fee_version.clone() + }, + PLATFORM_V16.fee_version, + "the 5.0 protocol version changes a fee group other than the smart-contract \ + pricing without inheriting it from v16" + ); + } + + /// The mock versions execute state transitions in drive-abci's protocol-upgrade suite and + /// one of them hand-writes its `SystemLimits`, so it is the one place the registry loop + /// above cannot reach. A mock that bounded contract computation would carry limits into a + /// suite that has no runtime to enforce them. + #[cfg(feature = "mock-versions")] + #[test] + fn mock_platform_versions_have_no_smart_contract_computation_limits() { + use crate::version::mocks::v2_test::TEST_PLATFORM_V2; + use crate::version::mocks::v3_test::TEST_PLATFORM_V3; + use crate::version::protocol_version::PLATFORM_TEST_VERSIONS; + + let versions = + PLATFORM_TEST_VERSIONS.get_or_init(|| vec![TEST_PLATFORM_V2, TEST_PLATFORM_V3]); + assert!( + !versions.is_empty(), + "the mock version registry is empty; this test would assert nothing" + ); + for platform_version in versions { + assert!( + platform_version + .system_limits + .smart_contract_computation + .is_none(), + "mock platform version {} bounds smart-contract computation", + platform_version.protocol_version + ); + assert!( + platform_version.fee_version.dashvm.is_none(), + "mock platform version {} prices smart-contract computation", + platform_version.protocol_version + ); + } + } + #[test] fn document_value_depth_limit_starts_at_protocol_version_13() { // v12 is already active on live networks, so the limit must not apply there. diff --git a/packages/rs-platform-version/src/version/system_limits/smart_contract.rs b/packages/rs-platform-version/src/version/system_limits/smart_contract.rs new file mode 100644 index 00000000000..c7d280efa15 --- /dev/null +++ b/packages/rs-platform-version/src/version/system_limits/smart_contract.rs @@ -0,0 +1,76 @@ +/// A deterministic count of smart-contract work. +/// +/// One unit is the weight the active metering generation assigns to one admitted guest +/// operation or one slice of host work performed on the guest's behalf (host entry, byte +/// copying, memory growth, native operation cost). The weights themselves live with the +/// metering generation, not here. A unit is never wall-clock time: two nodes that run the same +/// invocation under the same protocol version count exactly the same number of units, whatever +/// their hardware, cache state or compiler backend. +/// +/// Consumption is reported in this unit by the runtime and priced in credits by the active +/// protocol version's fee schedule (`platform_version.fee_version.dashvm`), see +/// `dpp::fee::smart_contract_computation`. +pub type ComputationUnits = u64; + +/// The consensus limits on smart-contract computation. +/// +/// Both limits are counted in [`ComputationUnits`] by one contract-only counter. That counter is +/// separate from every native budget: the proposer's wall-clock timer, the per-block withdrawal +/// and shielded caps and the Tenderdash block gas limit all keep their existing meaning and none +/// of them is derived from these numbers. +/// +/// # Per invocation +/// +/// `max_computation_units_per_invocation` bounds one outer contract invocation: a direct call, +/// a predicate evaluated for a native transition, or one scheduled attempt. Everything the +/// invocation causes is charged to that one counter, including module initialisation, nested +/// contract calls, predicates those calls trigger and the host work they request. Nothing is +/// counted twice: a nested call shares its caller's counter rather than opening a second one. +/// The runtime receives this value (or a smaller bound the caller declares and can afford) as +/// the budget of the invocation, meters guest and host work against it, and reports the units +/// actually consumed. A failed invocation still consumed its units and is charged for them. +/// +/// # Per block +/// +/// `max_computation_units_per_block` bounds the sum of all contract invocations in one block, +/// ordinary and scheduled together. The block loop reserves an invocation's admitted bound +/// against the block before it runs and settles the actual consumption afterwards, so an +/// invocation that would not fit is never started; it is delayed by the proposer and rejected by +/// a validator, never failed part-way through. The ledger that does this bookkeeping lives in +/// `drive-abci` (`BlockComputationBudget`). +/// +/// # Credits and gas +/// +/// Units become credits through the protocol-versioned price in the active protocol version's +/// fee schedule (`platform_version.fee_version.dashvm.credits_per_computation_unit`, checked +/// multiplication; the persisted epoch fee history never carries this group). The resulting +/// charge enters the processing fee of the invocation's `FeeResult`, which is what +/// Tenderdash's `gas_used` and `gas_wanted` already report. Gas therefore stays denominated in +/// credits; no unit equivalence between computation units and Tenderdash gas is introduced. +/// +/// # Versioning +/// +/// `None` on every protocol version that predates smart contracts: no code path meters, prices +/// or budgets contract computation there. The numbers on the first version that carries +/// `Some` are provisional until measured; see the `SYSTEM_LIMITS_V*` that sets them. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmartContractComputationLimits { + /// Most computation units one outer contract invocation may consume, counting every nested + /// call, predicate, module initialisation and host entry it causes. + pub max_computation_units_per_invocation: ComputationUnits, + /// Most computation units all contract invocations in one block may consume together, + /// ordinary and scheduled. + pub max_computation_units_per_block: ComputationUnits, +} + +impl SmartContractComputationLimits { + /// Whether the limits can be enforced at all: both are non-zero (a zero limit would reject + /// every invocation) and one invocation fits in a block (otherwise the per-invocation limit + /// could never be reached and the per-block reservation would refuse a maximal invocation). + /// This is the only invariant that must hold whatever the measured numbers turn out to be; + /// the table that sets the numbers asserts it at compile time. + pub const fn is_well_formed(&self) -> bool { + self.max_computation_units_per_invocation > 0 + && self.max_computation_units_per_block >= self.max_computation_units_per_invocation + } +} diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index a9b5e7fdbb6..0bba6fa95e7 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -83,4 +83,5 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { max_document_expirations_per_block: 0, max_document_expiration_weight_per_block: 0, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + smart_contract_computation: None, // smart contracts arrive with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index bb5d64573be..2511f5feb59 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -64,4 +64,5 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { max_document_expirations_per_block: 0, max_document_expiration_weight_per_block: 0, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + smart_contract_computation: None, // smart contracts arrive with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index b54132c50e2..dba5f957a06 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -66,4 +66,5 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { max_document_expirations_per_block: 0, max_document_expiration_weight_per_block: 0, minimum_grovedb_proof_envelope_version: 0, // V0 envelopes stay accepted until v14 + smart_contract_computation: None, // smart contracts arrive with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index a7b23123841..07e1edd680f 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -146,4 +146,5 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { max_document_expirations_per_block: 128, // document ttl (new in v14): expired documents deleted per block max_document_expiration_weight_per_block: 1_024, // document ttl (new in v14): deleted documents plus their index levels per block minimum_grovedb_proof_envelope_version: 1, // clients reject legacy V0 GroveDB proof envelopes from v14 + smart_contract_computation: None, // smart contracts arrive with the 5.0 protocol version }; diff --git a/packages/rs-platform-version/src/version/system_limits/v5.rs b/packages/rs-platform-version/src/version/system_limits/v5.rs new file mode 100644 index 00000000000..6509570f85b --- /dev/null +++ b/packages/rs-platform-version/src/version/system_limits/v5.rs @@ -0,0 +1,32 @@ +use crate::version::system_limits::smart_contract::SmartContractComputationLimits; +use crate::version::system_limits::v4::SYSTEM_LIMITS_V4; +use crate::version::system_limits::SystemLimits; + +/// System limits for protocol version 17 (5.0) and above. +/// +/// Identical to [`SYSTEM_LIMITS_V4`] except that smart-contract computation is bounded: one +/// outer invocation may consume at most 25 million computation units and all invocations in a +/// block at most 250 million. The numbers are the provisional starting values of the DashVM +/// shared allocation register (the "Compute" row); they are measured and revised before any +/// network is asked to run this protocol version. See `SmartContractComputationLimits` for what +/// a unit is and how the two limits are enforced. +pub const SYSTEM_LIMITS_V5: SystemLimits = SystemLimits { + smart_contract_computation: Some(SmartContractComputationLimits { + // provisional: allocation register "Compute" row, 25 million units per outer invocation + max_computation_units_per_invocation: 25_000_000, + // provisional: allocation register "Compute" row, 250 million contract units per block + max_computation_units_per_block: 250_000_000, + }), + ..SYSTEM_LIMITS_V4 +}; + +// Whatever the measured revision of the numbers above is, both limits stay non-zero and one +// maximal invocation still fits in a block; otherwise the per-block reservation could never admit +// an invocation that uses its full per-invocation budget. +const _: () = assert!( + match SYSTEM_LIMITS_V5.smart_contract_computation { + Some(ref limits) => limits.is_well_formed(), + None => false, + }, + "SYSTEM_LIMITS_V5 must carry well-formed smart-contract computation limits" +); diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs new file mode 100644 index 00000000000..95a317db9d5 --- /dev/null +++ b/packages/rs-platform-version/src/version/v15.rs @@ -0,0 +1,24 @@ +use crate::version::protocol_version::PlatformVersion; +use crate::version::v14::PLATFORM_V14; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; + +/// Placeholder for the 4.3 protocol version. +/// +/// The DashVM allocation register reserves protocol version 15 for the 4.3 release, 16 for 4.4 +/// and 17 for 5.0, and the registry is indexed by number, so the 5.0 version cannot exist on +/// this branch without 15 and 16. Until the 4.3 branch merges its real `v15.rs` forward this +/// version is identical to v14. +/// +/// It is written as a struct update rather than a copy of `v14.rs` on purpose: when the real +/// file arrives the add/add conflict is resolved by taking the incoming file, and because v16 +/// and v17 are struct updates over their predecessor every table the incoming version changes +/// flows into them without a second edit, except the two tables v17 overrides (`fee_version` +/// and `system_limits`, whose 5.0 generations are built on the tables current when they were +/// written). If the incoming version changes either of those, rebase the 5.0 generation onto +/// the incoming one and renumber it; the inheritance test in `system_limits` fails until then. +pub const PLATFORM_V15: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_15, + ..PLATFORM_V14 +}; diff --git a/packages/rs-platform-version/src/version/v16.rs b/packages/rs-platform-version/src/version/v16.rs new file mode 100644 index 00000000000..e3d80c4b16e --- /dev/null +++ b/packages/rs-platform-version/src/version/v16.rs @@ -0,0 +1,16 @@ +use crate::version::protocol_version::PlatformVersion; +use crate::version::v15::PLATFORM_V15; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_16: ProtocolVersion = 16; + +/// Placeholder for the 4.4 protocol version. +/// +/// Reserved by the DashVM allocation register (15 for 4.3, 16 for 4.4, 17 for 5.0). Identical to +/// v15 until the 4.4 branch merges its real `v16.rs` forward; see `v15.rs` for why it is a +/// struct update and how the forward merge is resolved. Should 4.4 ship no consensus change, the +/// register drops this activation and the 5.0 version becomes 16. +pub const PLATFORM_V16: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_16, + ..PLATFORM_V15 +}; diff --git a/packages/rs-platform-version/src/version/v17.rs b/packages/rs-platform-version/src/version/v17.rs new file mode 100644 index 00000000000..a741fee6bb0 --- /dev/null +++ b/packages/rs-platform-version/src/version/v17.rs @@ -0,0 +1,30 @@ +use crate::version::fee::v4::FEE_VERSION4; +use crate::version::protocol_version::PlatformVersion; +use crate::version::system_limits::v5::SYSTEM_LIMITS_V5; +use crate::version::v16::PLATFORM_V16; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_17: ProtocolVersion = 17; + +/// The 5.0 protocol version, the one that introduces smart contracts. Provisional number from +/// the DashVM allocation register (15 for 4.3, 16 for 4.4, 17 for 5.0). +/// +/// Two changes over v16 so far, both tables and both still unread by any dispatching code path: +/// +/// * `SYSTEM_LIMITS_V5` sets `smart_contract_computation`: at most 25 million computation +/// units per outer contract invocation and 250 million per block, counted by one +/// contract-only counter separate from every native budget. +/// * `FEE_VERSION4` prices those units at 1 credit each (`dashvm`); its number stays 1 because +/// no storage rate changes. +/// +/// Both numbers are provisional register values, measured and revised before any network is +/// asked to propose this version. Enforcement (the per-block ledger on the block execution +/// context, the not-executed classification for a full block, CheckTx affordability) arrives +/// with the tasks that wire the runtime in; until then a node at v17 behaves exactly like one +/// at v16. +pub const PLATFORM_V17: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_17, + fee_version: FEE_VERSION4, // changed: prices smart-contract computation (dashvm group) + system_limits: SYSTEM_LIMITS_V5, // changed: per-invocation and per-block smart-contract computation limits + ..PLATFORM_V16 +};