Skip to main content

PLATFORM_V14

Constant PLATFORM_V14 

pub const PLATFORM_V14: PlatformVersion;
Expand description

v14 hosts six consensus changes:

  1. Contract-level ranked aggregates: an index can declare that its groups are rankable by an aggregate, so a query like “top 5 restaurants by average grade” is served from an ordered secondary tree in O(log n + k) with a proof, instead of being rejected.
  2. The shared-prefix aggregate index fix: a data contract declaring an aggregating (countable / summable) index that terminates at a property which is also the prefix of a compound index (e.g. summable [a] next to [a, b]) registered successfully but rejected every document insert for most flag combinations, because Drive could not legally hang the compound continuation tree under the aggregating per-value tree. The v2 document index walkers (plus the v1 update walker) that fix it gate here as well: tree types derive through a shared continuation-demotion helper (provable count-bearing value trees with compound continuations demote to CountSumTree, since grovedb rejects count-suppressed children under provable count parents by design) and continuation inserts route through the completed zero-contribution wrapper matrix. No state migration is needed: shapes without compound continuations produce bit-identical operations, the broken shapes could never hold documents, and the one previously-insertable shape the demotion changes (a provable count-bearing value tree whose continuations were all sum-bearing — insertable pre-v14 only through an unenforced grovedb batch guard) simply gets CountSumTree value trees for values first seen at v14+, which readers treat identically.
  3. The contested vote poll index cross-check: the index named by a document create transition’s prefunded voting balance keys the vote poll, its stored info, its end-date entry and its prefunded specialized balance, while the contested index the contender is inserted under always comes from the document type. Up to v13 nothing tied the two together, so a submitter could register and fund a contest under a vote poll describing a different index than the one the contest was created on — which halts the chain when that poll ends — or open a contest for a document that is not a contested resource at all. State validation also prevents a non-contested create from occupying a live contested document’s id before the contest winner is awarded into primary storage. Drive’s contested insert also recreates an abstain or lock vote tree over the storage an earlier poll’s cleanup left orphaned (it only removed the trees that received votes), so a resource can be contested again instead of failing with CorruptedContractIndexes.
  4. Relative daily withdrawal limit: the flat 2000 Dash per 24 hours that applied from v8 becomes 15% of the total credits Platform held a day ago (SYSTEM_LIMITS_V4.daily_withdrawal_limit_percent, read by daily_withdrawal_limit v2 through DPP_METHOD_VERSIONS_V3), never below one maximal withdrawal (max_withdrawal_amount) so every accepted withdrawal eventually fits and cannot block the pooling queue. The base is capped at max_daily_withdrawal_amount (4000 Dash, Core’s unlock capacity per day under V24 as written); the credit inflows of the active window — every credit mint, recorded per block by record_credit_inflows_for_withdrawals in the credit inflows sum tree — are added after the cap, so the limit counts net outflow and a matching deposit -> withdraw cycle does not consume the capped budget of other users (#4471). Outflow funded by same-window deposits may therefore exceed the cap; this mirrors the net credit-pool rule Core adopts for V24 alongside this change (tracked in #4471), which must land before V24 activates. Both the inflows and the pooled reservations count over the interval after the base snapshot only — an entry the snapshot already reflects is neither added nor subtracted again. The base is the total credits recorded at the latest block at least 24 hours before the current one: DRIVE_ABCI_METHOD_VERSIONS_V10 turns on record_total_credits_history_for_withdrawals, which checks the total credits every block once fees and epoch rewards are in, writes it under the withdrawals tree keyed by block time whenever it changed (an entry describes the total until the next one) and prunes entries older than the one the limit reads, and DRIVE_VERSION_V9’s identity withdrawal table bumps calculate_current_withdrawal_limit to 1 to read that lagged value. Until an entry is a day old — the first day after activation — the flat 2000 Dash keeps applying, so the lag cannot be skipped by inflating the total before or at activation. The lag is the guardrail: a sudden jump in the total credits does not raise the limit for a day. Amounts already pooled in the last 24 hours keep counting against the maximum exactly as before. Pre-V24 Core caps unlocks at LimitAmountV22 (2000 Dash) per block, with the amount checked only at block level, so any daily total is still minable across blocks; V24’s 4000 Dash per 576-block window matches the capped base and is raised to the same net rule before activation (see above).
  5. Time-range indexes: an index can declare a timeRange transform that buckets a required system timestamp ($createdAt / $updatedAt / $transferredAt) into fixed-length, regularly-spaced, optionally overlapping windows declared in seconds (range / step, plus an optional phase < step alignment offset). Each grid gets its own index subtree — the level is keyed by the property name qualified with the grid — so several grids may bucket one timestamp side by side. A document is stored once per containing bucket per grid (the v2 insert/delete and v1 update walkers carry the fan-out; the per-document write amplification is capped per index by SystemLimits::max_time_range_overlap_factor), and the v1 getDocuments handler resolves the new IN_TIME_RANGE operator — a typed TimeRangeSelection operand: NEWEST/OLDEST (resolved to a bucket-start equality from committed block time) or BY_START (naming any window, current or historic, by its grid-aligned start), with a grid member naming one grid where several bucket the field — making trending/leaderboard document and count/sum/avg queries provable over the current or any named window. unique: true is admitted only for non-overlapping windows (range == step) sourced from the immutable $createdAt.
  6. Deterministic token reward math: DistributionFunction::evaluate (logarithmic, inverted-logarithmic, exponential and polynomial perpetual distributions) computes ln/exp/pow through the pinned pure-Rust libm crate instead of the platform C library. musl’s log takes an FMA path on aarch64 and a non-FMA path on x86_64, so the two disagree by 1 ulp on some inputs; a contract owner could pick parameters whose reward sat within that ulp of an integer, and floor then minted different amounts on the two architectures, splitting the app hash both at claim time and at contract registration (validation evaluates the start value). Gated on distribution_function_evaluate_version so both architectures switch at the same height; pre-v14 blocks replay on the old math byte-for-byte. log/exp have no architecture dispatch and pow’s only arch-touching call is the correctly-rounded sqrt, so the result is bit-identical on every target Platform builds for. The goal is determinism, not correct rounding: on a boundary tuple the host libm (glibc, macOS) can still be 1 ulp away, so anything predicting rewards with host math may differ from consensus by one unit.

The first two are orthogonal by construction: the ranked upgrade decides the property-name tree type, the demotion decides the value tree type one level below it, and a demoted CountSumTree value tree contributes its (count, sum) to a ranked indexed parent exactly as the provable variant did — so ranked secondaries keep ranking correctly over shared-prefix shapes.

Until a contract uses the ranked or time-range grammar, the only v14 behavior changes are the shared-prefix fix, the contested-index cross-check, the index-reorder schema-compatibility fix and the relative daily withdrawal limit; everything else matches v13:

  • CONTRACT_VERSIONS_V6 points document_type_schema at the v3 document meta-schema, which hosts the ranked index keywords (rankedCountable / rankedSummable / rankedAverageable), the refersTo reference keyword and the timeRange index transform. v13 keeps validating against meta-schema v2, where those keys are rejected as unknown properties, so a pre-v14 contract cannot smuggle them in. It also bumps validate_schema_compatibility to 1, which strips the top-level indices key before diffing the old and new document type schemas: index immutability is enforced by validate_update v1’s name-keyed comparison, so a contract update that merely reorders the indices array validates cleanly instead of hitting the unsupported-keyword hard error (an internal error under v13).
  • DRIVE_VERSION_V9 carries DRIVE_DOCUMENT_METHOD_VERSIONS_V4, adding the detect_ranked_mode routing slot, plus the grove-method slots for creating the three indexed tree variants and the verify-method slot for verify_ranked_top_k_proof. All are 0 today. The same table bumps the four index walkers to v2 and the document update walker to v1 for the shared-prefix fix; those same walker versions carry the time-range bucket fan-out, so both features gate on one table entry.
  • DRIVE_ABCI_QUERY_VERSIONS_V3 bumps document_query_helpers.compute_aggregate_mode_and_check_limit 0 → 2, opening two routes on the v1 document-query handler: the ranked path (a grouped aggregate whose single order_by names the selected aggregate — ORDER BY <agg> [ASC|DESC] LIMIT n [OFFSET m]) and the boolean-HAVING range path (a grouped aggregate carrying exactly one having clause on the selected aggregate — GROUP BY p HAVING <agg> <op> <value> LIMIT n), the latter served as a value-bounded range read of the covering ranked index’s axis secondary. v13 and earlier keep the v1 table and therefore keep rejecting both shapes, so mixed-version networks agree across the upgrade.
  • DRIVE_ABCI_VALIDATION_VERSIONS_V10 bumps document_create_transition_structure_validation 0 → 1, requiring a contested create transition’s prefunded voting balance to name the same vote poll the document itself resolves to, and rejecting one on a document that resolves to no contested index. It also bumps document create state validation to 2, enforcing refersTo document references and rejecting a non-contested create whose id is already present in the contested tree. Document replace state validation 1 enforces the same reference checks. v13 keeps the v9 table and therefore keeps accepting all of these, so replay of pre-upgrade blocks is unchanged.
  • DOCUMENT_VERSIONS_V4 bumps document_serialization_version to default 3: documents are stamped with the contract version their bytes conform to (a varint after the format prefix), enabling the requiredSince property keyword — a contract update may add a new required property annotated with the version that update creates. Documents stamped below a property’s requiredSince keep the presence-flagged layout they were written with, so the latest contract alone reconstructs every stamp’s layout and no historical contract lookups are ever needed. Reads dispatch on the byte prefix, so formats 0–2 (all pre-v14 documents) deserialize exactly as before with an unstamped (pre-annotation) layout.
  1. Client-side GroveDB proof envelope floor: SYSTEM_LIMITS_V4.minimum_grovedb_proof_envelope_version becomes 1, so a client verifying with v14 tables rejects the legacy V0 proof envelope before its bytes reach Drive (drive-proof-verifier, wasm-drive-verify, and the nested compacted address proofs). V0’s item binding lets a prover return different item bytes under the same authenticated root; every live network has emitted V1 envelopes since v13 (grove version 3), so no honest response is affected.
  2. Epoch-based perpetual distribution claims stop wrapping: RewardDistributionType::max_cycle_moment (the cap on how far one claim may redeem, selected by TOKEN_VERSIONS_V3.reward_distribution_max_cycle_moment_version 1) computes start + interval * cycles in u64 with saturating arithmetic and narrows back to EpochIndex only after capping at the last completed cycle moment (current cycle moment - interval, the previous epoch for an interval of one as before; for wider intervals the same cycles are paid, but the cap now sits on a cycle boundary, the only shape in which evaluate_interval’s fixed-amount step count and its per-cycle loop agree). Up to v13 the sum was taken in u16: a fixed-amount function allows 32,767 cycles, so any epoch interval of three or more with a nonzero start (or two with a start at epoch two or later) pushed the cap past u16::MAX. Release builds wrap, the cap landed below the start, evaluate_interval saw an empty range and the claim was refused with InvalidTokenClaimNoCurrentRewards on every attempt. The v0 arithmetic is kept, wrapping explicitly, so those refusals replay.
  3. Evonode reward cycles weighted by the epochs they span: the per-cycle evaluator in DistributionFunction::evaluate_interval asks the participation ratio for the epochs a cycle covers (TOKEN_VERSIONS_V3.distribution_function_cycle_epochs_version 1: cycle moment - interval + 1 ..= cycle moment). Up to v13 it passed the cycle’s step index as if it were an epoch, which coincides only for an interval of one; for a wider interval it named epochs before the distribution started, outside the epoch window the claim loads, and an EvonodesByParticipation claim with a function other than a fixed amount failed as an internal error (reachable only once item 8 let the cap stop wrapping). Interval-one distributions are unchanged.
  4. A zero epoch interval is rejected at registration: RewardDistributionType::validate_structure_interval v1 (CONTRACT_VERSIONS_V6.token_versions.validate_structure_interval) refuses an EpochBasedDistribution with interval: 0 with the new InvalidTokenDistributionEpochIntervalTooShortError (code 10828) on contract create and update. Up to v13 the epoch arm enforced nothing, so such a contract registered and every claim on it failed as an internal error, since no cycle can be computed from a zero step. Block and time minimums are unchanged.
  • ShieldFromIdentity (state transition type 21) activates: SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION = 14 gates it in is_allowed, and DRIVE_ABCI_VALIDATION_VERSIONS_V10 is the first table whose shield_from_identity_state_transition row enables basic structure, identity signature, and nonce validation. It moves credits from an identity balance straight into the shielded pool: the funding side is identity-signed like IdentityCreditTransferToAddresses, the pool side is an outputs-only Orchard bundle like Shield, and the fee is metered plus the shielded compute fee, paid from the identity.

  • IdentityTopUpFromShieldedPool (state transition type 22) activates at the same gate (IDENTITY_TOP_UP_FROM_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 14, DRIVE_ABCI_VALIDATION_VERSIONS_V10 row). It spends shielded notes like Unshield and credits an EXISTING identity’s balance instead of a platform address: pool-paid flat fee (compute_shielded_identity_top_up_fee), no platform signature, the target identity and gross amount bound into the Orchard sighash, and no system-credit adjustment (pool and identity balances are both conservation-equation terms).

The wire surface changes only additively: GetDocumentsRequestV1 already carries selects / group_by / order_by / limit / offset; the ranked response is an additive ResultData.ranked variant, whose skipped field is likewise additive; and the v1 where-clause operator enum gains IN_TIME_RANGE = 11, which pre-v14 servers reject as an unknown operator rather than misread (the v0 wire has no time-range operator at all). Contract-bound authentication keys activate through contract-bounds validation v2, identity-signature validation v1 and batch advanced-structure v1. Identity creation validates key bounds (state v1) and identity-update state v1 retains the contract lookup fees; Drive identity methods v2 index and refresh the bound keys. Contract group bounds on authentication keys ride the same versions: contract-bounds validation v2 admits them, batch transform v2 resolves the member contract’s group memberships into the action (only for a group-bound signing key) for advanced-structure v1 to judge, and shielded-proof validation v1 refuses them in identity creation from the shielded pool, whose sighash preimage layout predates them. A transition carrying such a key is inactive before this version (active_version_range), so earlier protocol versions reject it without charging, as a binary that cannot decode it does. Authentication keys may carry a budget and an expiry (the version 1 public key format, which StateTransition::active_version_range admits from 14). Key structure validation v1 (STATE_TRANSITION_METHOD_VERSIONS_V2) and validate_identity_public_keys_limits decide which keys may carry them; Drive identity methods v2 write the remaining budget when the key is added; identity-signature validation v1 refuses a key whose budget is spent; validate_fees_of_event v1 refuses an expired key and a spend the remaining budget does not cover (only metered processing may overshoot); execute_event v1 deducts what was spent. Shielded-proof validation v1 refuses a key that carries a budget or an expiry in identity creation from the shielded pool, whose sighash preimage does not cover the limits. IdentityKeyLimitsUpdate (state transition type 23, gated by IDENTITY_KEY_LIMITS_UPDATE_INITIAL_PROTOCOL_VERSION) raises a key’s total budget, and the remaining budget with it, or moves its expiry later; it only ever loosens limits. Signed by a MASTER key or by a CRITICAL key without limits (DRIVE_ABCI_VALIDATION_VERSIONS_V10 turns its gates on; Drive identity methods v2 rewrite the key and raise the remaining budget).