Skip to main content

platform_version/version/
v14.rs

1use crate::version::consensus_versions::ConsensusVersions;
2use crate::version::dpp_versions::dpp_asset_lock_versions::v1::DPP_ASSET_LOCK_VERSIONS_V1;
3use crate::version::dpp_versions::dpp_contract_versions::v6::CONTRACT_VERSIONS_V6;
4use crate::version::dpp_versions::dpp_costs_versions::v1::DPP_COSTS_VERSIONS_V1;
5use crate::version::dpp_versions::dpp_document_versions::v4::DOCUMENT_VERSIONS_V4;
6use crate::version::dpp_versions::dpp_factory_versions::v1::DPP_FACTORY_VERSIONS_V1;
7use crate::version::dpp_versions::dpp_identity_versions::v1::IDENTITY_VERSIONS_V1;
8use crate::version::dpp_versions::dpp_method_versions::v3::DPP_METHOD_VERSIONS_V3;
9use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2;
10use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1;
11use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3;
12use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3;
13use crate::version::dpp_versions::dpp_token_versions::v2::TOKEN_VERSIONS_V2;
14use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5;
15use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2;
16use crate::version::dpp_versions::DPPVersion;
17use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1;
18use crate::version::drive_abci_versions::drive_abci_method_versions::v10::DRIVE_ABCI_METHOD_VERSIONS_V10;
19use crate::version::drive_abci_versions::drive_abci_query_versions::v3::DRIVE_ABCI_QUERY_VERSIONS_V3;
20use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1;
21use crate::version::drive_abci_versions::drive_abci_validation_versions::v10::DRIVE_ABCI_VALIDATION_VERSIONS_V10;
22use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v3::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3;
23use crate::version::drive_abci_versions::DriveAbciVersion;
24use crate::version::drive_versions::v9::DRIVE_VERSION_V9;
25use crate::version::fee::v2::FEE_VERSION2;
26use crate::version::protocol_version::PlatformVersion;
27use crate::version::system_data_contract_versions::v3::SYSTEM_DATA_CONTRACT_VERSIONS_V3;
28use crate::version::system_limits::v4::SYSTEM_LIMITS_V4;
29use crate::version::ProtocolVersion;
30
31pub const PROTOCOL_VERSION_14: ProtocolVersion = 14;
32
33/// v14 hosts five consensus changes:
34///
35/// 1. **Contract-level ranked aggregates** (this branch): an index can
36///    declare that its groups are rankable by an aggregate, so a query like
37///    "top 5 restaurants by average grade" is served from an ordered
38///    secondary tree in O(log n + k) with a proof, instead of being rejected.
39/// 2. **The shared-prefix aggregate index fix**: a data contract declaring
40///    an aggregating (countable / summable) index that terminates at a
41///    property which is also the prefix of a compound index (e.g. summable
42///    `[a]` next to `[a, b]`) registered successfully but rejected every
43///    document insert for most flag combinations, because Drive could not
44///    legally hang the compound continuation tree under the aggregating
45///    per-value tree. The v2 document index walkers (plus the v1 update
46///    walker) that fix it gate here as well: tree types derive through a
47///    shared continuation-demotion helper (provable count-bearing value
48///    trees with compound continuations demote to `CountSumTree`, since
49///    grovedb rejects count-suppressed children under provable count
50///    parents by design) and continuation inserts route through the
51///    completed zero-contribution wrapper matrix. No state migration is
52///    needed: shapes without compound continuations produce bit-identical
53///    operations, the broken shapes could never hold documents, and the
54///    one previously-insertable shape the demotion changes (a provable
55///    count-bearing value tree whose continuations were all sum-bearing —
56///    insertable pre-v14 only through an unenforced grovedb batch guard)
57///    simply gets `CountSumTree` value trees for values first seen at
58///    v14+, which readers treat identically.
59/// 3. **The contested vote poll index cross-check**: the index named by a
60///    document create transition's prefunded voting balance keys the vote
61///    poll, its stored info, its end-date entry and its prefunded
62///    specialized balance, while the contested index the contender is
63///    inserted under always comes from the document type. Up to v13 nothing
64///    tied the two together, so a submitter could register and fund a
65///    contest under a vote poll describing a different index than the one
66///    the contest was created on — which halts the chain when that poll
67///    ends — or open a contest for a document that is not a contested
68///    resource at all.
69/// 4. **Relative daily withdrawal limit**: the flat 2000 Dash per 24 hours that
70///    applied from v8 becomes 15% of the total credits Platform held a day ago
71///    (`SYSTEM_LIMITS_V4.daily_withdrawal_limit_percent`, read by
72///    `daily_withdrawal_limit` v2 through `DPP_METHOD_VERSIONS_V3`), never below
73///    one maximal withdrawal (`max_withdrawal_amount`) so every accepted
74///    withdrawal eventually fits and cannot block the pooling queue. The base is
75///    capped at `max_daily_withdrawal_amount` (4000 Dash, Core's unlock capacity
76///    per day under V24 as written); the credit inflows of the active window —
77///    every credit mint, recorded per block by
78///    `record_credit_inflows_for_withdrawals` in the credit inflows sum tree —
79///    are added after the cap, so the limit counts net outflow and a matching
80///    deposit -> withdraw cycle does not consume the capped budget of other
81///    users (#4471). Outflow funded by same-window deposits may therefore
82///    exceed the cap; this mirrors the net credit-pool rule Core adopts for V24
83///    alongside this change (tracked in #4471), which must land before V24
84///    activates. Both the inflows and the pooled reservations count over the
85///    interval after the base snapshot only — an entry the snapshot already
86///    reflects is neither added nor subtracted again. The base is
87///    the total credits recorded at the latest block at least 24 hours before
88///    the current one: `DRIVE_ABCI_METHOD_VERSIONS_V10` turns on
89///    `record_total_credits_history_for_withdrawals`, which checks the total
90///    credits every block once fees and epoch rewards are in, writes it under
91///    the withdrawals tree keyed by block time whenever it changed (an entry
92///    describes the total until the next one) and prunes entries older than the
93///    one the limit reads, and `DRIVE_VERSION_V9`'s identity withdrawal table
94///    bumps `calculate_current_withdrawal_limit` to 1 to read that lagged
95///    value. Until an entry is a day old — the first day after activation — the
96///    flat 2000 Dash keeps applying, so the lag cannot be skipped by inflating
97///    the total before or at activation. The lag is the guardrail: a sudden
98///    jump in the total credits does not raise the limit for a day. Amounts
99///    already pooled in the last 24 hours keep counting against the maximum
100///    exactly as before. Pre-V24 Core caps unlocks at `LimitAmountV22` (2000
101///    Dash) per *block*, with the amount checked only at block level, so any
102///    daily total is still minable across blocks; V24's 4000 Dash per 576-block
103///    window matches the capped base and is raised to the same net rule before
104///    activation (see above).
105/// 5. **Time-range indexes**: an index can declare a `timeRange` transform
106///    that buckets a required system timestamp (`$createdAt` /
107///    `$updatedAt` / `$transferredAt`) into fixed-length, regularly-spaced,
108///    optionally overlapping windows declared in seconds (`range` / `step`,
109///    plus an optional `phase < step` alignment offset). Each grid gets its
110///    own index subtree — the level is keyed by the property name qualified
111///    with the grid — so several grids may bucket one timestamp side by
112///    side. A document is stored once per containing bucket per grid (the
113///    v2 insert/delete and v1 update walkers carry the fan-out; the
114///    per-document write amplification is capped per index by
115///    `SystemLimits::max_time_range_overlap_factor`), and the v1
116///    `getDocuments` handler resolves the new `IN_TIME_RANGE` operator —
117///    bare `"newest"`/`"oldest"` on a single-grid field, or a structured
118///    `[selector, range, step(, phase)]` operand naming one grid — into a
119///    bucket-start equality from committed block time, making "newest
120///    window" trending/leaderboard document and count/sum/avg queries
121///    provable. `unique: true` is admitted only for non-overlapping windows
122///    (`range == step`) sourced from the immutable `$createdAt`.
123///
124/// The first two are orthogonal by construction: the ranked upgrade decides the
125/// *property-name* tree type, the demotion decides the *value* tree type
126/// one level below it, and a demoted `CountSumTree` value tree contributes
127/// its (count, sum) to a ranked indexed parent exactly as the provable
128/// variant did — so ranked secondaries keep ranking correctly over
129/// shared-prefix shapes.
130///
131/// Until a contract uses the ranked or time-range grammar, the only v14
132/// behavior changes are the shared-prefix fix, the contested-index
133/// cross-check, the index-reorder schema-compatibility fix and the relative
134/// daily withdrawal limit; everything else matches v13:
135///
136/// * `CONTRACT_VERSIONS_V6` points `document_type_schema` at the v3 document
137///   meta-schema, which hosts the ranked index keywords
138///   (`rankedCountable` / `rankedSummable` / `rankedAverageable`), the
139///   `refersTo` reference keyword and the `timeRange` index transform. v13
140///   keeps validating against meta-schema v2, where those keys are rejected
141///   as unknown properties, so a pre-v14 contract cannot smuggle them in.
142///   It also bumps `validate_schema_compatibility` to 1, which strips the
143///   top-level `indices` key before diffing the old and new document type
144///   schemas: index immutability is enforced by `validate_update` v1's
145///   name-keyed comparison, so a contract update that merely reorders the
146///   `indices` array validates cleanly instead of hitting the
147///   unsupported-keyword hard error (an internal error under v13).
148/// * `DRIVE_VERSION_V9` carries `DRIVE_DOCUMENT_METHOD_VERSIONS_V4`, adding
149///   the `detect_ranked_mode` routing slot, plus the grove-method slots for
150///   creating the three indexed tree variants and the verify-method slot for
151///   `verify_ranked_top_k_proof`. All are 0 today. The same table bumps the
152///   four index walkers to v2 and the document update walker to v1 for the
153///   shared-prefix fix; those same walker versions carry the time-range
154///   bucket fan-out, so both features gate on one table entry.
155/// * `DRIVE_ABCI_QUERY_VERSIONS_V3` bumps
156///   `document_query_helpers.compute_aggregate_mode_and_check_limit` 0 → 2,
157///   opening two routes on the v1 document-query handler: the ranked path
158///   (a grouped aggregate whose single `order_by` names the selected
159///   aggregate — `ORDER BY <agg> [ASC|DESC] LIMIT n [OFFSET m]`) and the
160///   boolean-`HAVING` range path (a grouped aggregate carrying exactly one
161///   `having` clause on the selected aggregate — `GROUP BY p HAVING <agg>
162///   <op> <value> LIMIT n`), the latter served as a value-bounded range
163///   read of the covering ranked index's axis secondary. v13 and earlier
164///   keep the v1 table and therefore keep rejecting both shapes, so
165///   mixed-version networks agree across the upgrade.
166/// * `DRIVE_ABCI_VALIDATION_VERSIONS_V10` bumps
167///   `document_create_transition_structure_validation` 0 → 1, requiring a
168///   contested create transition's prefunded voting balance to name the
169///   same vote poll the document itself resolves to, and rejecting one on a
170///   document that resolves to no contested index. It also bumps document
171///   create state validation to 2 and document replace state validation to
172///   1, enforcing `refersTo` document references: a document whose
173///   reference property names an identity or contract that does not exist
174///   is rejected. v13 keeps the v9 table and therefore keeps
175///   accepting all of these, so replay of pre-upgrade blocks is unchanged.
176/// * `DOCUMENT_VERSIONS_V4` bumps `document_serialization_version` to
177///   default 3: documents are stamped with the contract version their bytes
178///   conform to (a varint after the format prefix), enabling the
179///   `requiredSince` property keyword — a contract update may add a new
180///   required property annotated with the version that update creates.
181///   Documents stamped below a property's `requiredSince` keep the
182///   presence-flagged layout they were written with, so the latest contract
183///   alone reconstructs every stamp's layout and no historical contract
184///   lookups are ever needed. Reads dispatch on the byte prefix, so
185///   formats 0–2 (all pre-v14 documents) deserialize exactly as before with
186///   an unstamped (pre-annotation) layout.
187///
188/// The wire surface changes only additively: `GetDocumentsRequestV1`
189/// already carries `selects` / `group_by` / `order_by` / `limit` /
190/// `offset`; the ranked response is an additive `ResultData.ranked`
191/// variant, whose `skipped` field is likewise additive; and the v1
192/// where-clause operator enum gains `IN_TIME_RANGE = 11`, which pre-v14
193/// servers reject as an unknown operator rather than misread (the v0 wire
194/// has no time-range operator at all).
195pub const PLATFORM_V14: PlatformVersion = PlatformVersion {
196    protocol_version: PROTOCOL_VERSION_14,
197    drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot
198    drive_abci: DriveAbciVersion {
199        structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1,
200        methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit
201        validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation
202        withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history
203        query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time
204        checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1,
205    },
206    dpp: DPPVersion {
207        costs: DPP_COSTS_VERSIONS_V1,
208        validation: DPP_VALIDATION_VERSIONS_V5,
209        state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire
210        state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2,
211        state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1,
212        state_transitions: STATE_TRANSITION_VERSIONS_V3,
213        contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords
214        document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties
215        identity_versions: IDENTITY_VERSIONS_V1,
216        voting_versions: VOTING_VERSION_V2,
217        token_versions: TOKEN_VERSIONS_V2,
218        asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1,
219        methods: DPP_METHOD_VERSIONS_V3, // changed: daily_withdrawal_limit v2 — a percentage of the total credits a day ago
220        factory_versions: DPP_FACTORY_VERSIONS_V1,
221    },
222    system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, // changed: DashPay v2 adds profile payment address fields (DIP-33)
223    fee_version: FEE_VERSION2,
224    system_limits: SYSTEM_LIMITS_V4, // changed: daily withdrawal limit becomes 15% of the total credits a day ago + time-range overlap-factor cap (24)
225    consensus: ConsensusVersions {
226        tenderdash_consensus_version: 1,
227    },
228};
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::version::v13::PLATFORM_V13;
234
235    /// The ranked / boolean-HAVING routing gate lives in v14's own query
236    /// table, so flipping it touches only v14: a v13 node keeps running
237    /// the v0 helper, which rejects every non-empty HAVING, so a
238    /// mixed-version network agrees until the upgrade vote carries.
239    ///
240    /// v14 selects the v2 helper, which routes the ranked shape
241    /// (`ORDER BY <agg> LIMIT n`) to `dispatch_ranked_v1` and the
242    /// boolean-HAVING range shape (exactly one `having` clause on the
243    /// selected aggregate) to `dispatch_having_v1`. A change that made
244    /// v13 non-zero here would be consensus-breaking for
245    /// already-deployed nodes, which is exactly what the v13 half of
246    /// this assertion guards.
247    #[test]
248    fn ranked_having_routing_gate_is_v14_only() {
249        assert_eq!(
250            PLATFORM_V13
251                .drive_abci
252                .query
253                .document_query_helpers
254                .compute_aggregate_mode_and_check_limit,
255            0
256        );
257        assert_eq!(
258            PLATFORM_V14
259                .drive_abci
260                .query
261                .document_query_helpers
262                .compute_aggregate_mode_and_check_limit,
263            2
264        );
265    }
266
267    /// The ranked index keywords are gated by the meta-schema version, so v14
268    /// must select meta-schema v3 while v13 stays on v2.
269    #[test]
270    fn ranked_index_keywords_are_gated_by_meta_schema_v3() {
271        assert_eq!(
272            PLATFORM_V13
273                .dpp
274                .contract_versions
275                .document_type_versions
276                .schema
277                .document_type_schema,
278            2
279        );
280        assert_eq!(
281            PLATFORM_V14
282                .dpp
283                .contract_versions
284                .document_type_versions
285                .schema
286                .document_type_schema,
287            3
288        );
289    }
290
291    /// The ranked grammar lives in its own document-type parser generation
292    /// rather than behind a version gate inside a shipped one, so v14 must
293    /// select generation 3 while v13 stays on generation 2. Pinned here
294    /// because it is the whole reason generations 0/1/2 can stay byte-identical
295    /// to what consensus already ran: a historical block replayed at v13 is
296    /// parsed by a generation that has never heard of the ranked keywords.
297    /// The grove v4 cleanup gates (batch overwrite inspection + delete-tree
298    /// actual-type cleanup) exist for the indexed trees that ranked indexes
299    /// lay down, so v14 must select grove protocol 4 while v13 stays on 3.
300    /// The gates are cost-neutral — they derive the old element from data the
301    /// merk apply already loads — and the fee-constant tests pin identical
302    /// fees on both sides of the boundary. Platform flows cannot themselves
303    /// overwrite a ranked index (the flags are immutable on contract update
304    /// and new indexes cannot be added to an existing document type), so the
305    /// cleanup behavior itself is exercised by grovedb's own overwrite suites
306    /// at the pinned revision; this test pins that v14 actually activates
307    /// them.
308    #[test]
309    fn grove_v4_cleanup_gates_activate_at_v14() {
310        assert_eq!(PLATFORM_V13.drive.grove_version.protocol_version, 3);
311        assert_eq!(PLATFORM_V14.drive.grove_version.protocol_version, 4);
312    }
313
314    #[test]
315    fn ranked_grammar_gets_its_own_parser_generation() {
316        assert_eq!(
317            PLATFORM_V13
318                .dpp
319                .contract_versions
320                .document_type_versions
321                .class_method_versions
322                .try_from_schema,
323            2
324        );
325        assert_eq!(
326            PLATFORM_V14
327                .dpp
328                .contract_versions
329                .document_type_versions
330                .class_method_versions
331                .try_from_schema,
332            3
333        );
334    }
335
336    /// v14 introduces the slots but activates none of them yet. If a later
337    /// change flips one of these, it must do so deliberately — and update this
338    /// test — rather than by inheriting a default.
339    #[test]
340    fn ranked_feature_slots_exist_but_are_dormant() {
341        assert_eq!(
342            PLATFORM_V14.drive.methods.document.query.detect_ranked_mode,
343            0
344        );
345        assert_eq!(
346            PLATFORM_V14.drive.methods.document.query.detect_having_mode,
347            0
348        );
349        assert_eq!(
350            PLATFORM_V14
351                .drive
352                .methods
353                .verify
354                .document_ranked
355                .verify_ranked_top_k_proof,
356            0
357        );
358        assert_eq!(
359            PLATFORM_V14
360                .drive
361                .methods
362                .verify
363                .document_ranked
364                .verify_having_range_proof,
365            0
366        );
367        let grove = &PLATFORM_V14.drive.grove_methods.batch;
368        assert_eq!(grove.batch_insert_empty_provable_count_indexed_tree, 0);
369        assert_eq!(grove.batch_insert_empty_provable_sum_indexed_tree, 0);
370        assert_eq!(
371            grove.batch_insert_empty_provable_count_provable_sum_indexed_tree,
372            0
373        );
374    }
375
376    /// The contested vote poll index cross-check changes accept/reject
377    /// behavior for document create transitions, so it lives in v14's own
378    /// validation table: a v13 node keeps running structure validation v0,
379    /// which validates only the prefunded amount and ignores the index name.
380    /// A change that made v13 non-zero here would retroactively reject
381    /// transitions already in the chain.
382    #[test]
383    fn contested_index_cross_check_is_v14_only() {
384        assert_eq!(
385            PLATFORM_V13
386                .drive_abci
387                .validation_and_processing
388                .state_transitions
389                .batch_state_transition
390                .document_create_transition_structure_validation,
391            0
392        );
393        assert_eq!(
394            PLATFORM_V14
395                .drive_abci
396                .validation_and_processing
397                .state_transitions
398                .batch_state_transition
399                .document_create_transition_structure_validation,
400            1
401        );
402    }
403}