Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
eeb63e9
feat(platform): version tables for compilation readiness at protocol 17
DCG-Claude Sep 30, 2026
67b6465
feat(dpp): compilation readiness round, report record, scan cursor an…
DCG-Claude Sep 17, 2026
f857e9a
feat(drive): compilation readiness rounds, reports, cursors, deadline…
DCG-Claude Sep 17, 2026
fd5c389
test(drive): compilation readiness storage tests
DCG-Claude Sep 17, 2026
ae2d947
feat(drive-abci): create the compilation readiness structures on upgr…
DCG-Claude Sep 30, 2026
539439d
refactor(drive): type the retired readiness queue entry
DCG-Claude Sep 17, 2026
fdf9d81
fix(drive): refund a replaced readiness round to its own payer
DCG-Claude Sep 29, 2026
5524da3
fix(drive): remove an emptied readiness deadline bucket on retirement
DCG-Claude Sep 29, 2026
568a851
fix(drive): price readiness round cancellation in estimation mode
DCG-Claude Sep 29, 2026
af9a6e9
docs(drive): describe readiness refunds, deadline trees and estimates
DCG-Claude Sep 29, 2026
7685155
fix(drive): price the estimated readiness pool credit as the widest s…
DCG-Claude Sep 29, 2026
833f102
fix(drive): price an estimated readiness fund insert as the widest su…
DCG-Claude Sep 30, 2026
4b20cb1
fix(drive): estimate readiness round activation without reading state
DCG-Claude Sep 30, 2026
be6f95c
fix(drive): bound the estimate of a readiness cleanup step
DCG-Claude Sep 30, 2026
d2a5d77
fix(dpp): keep the readiness scan cursor unchanged when advancing ove…
DCG-Claude Sep 30, 2026
16cda84
fix(drive): publish a readiness crossing only after its operations ar…
DCG-Claude Sep 30, 2026
404e614
fix(drive): size readiness estimates for the widest serialized records
DCG-Claude Sep 30, 2026
793412e
fix(drive): refuse to reopen the current readiness round
DCG-Claude Sep 30, 2026
e4ca4aa
refactor(drive): version the readiness pool credit helper
DCG-Claude Sep 30, 2026
6efbe89
fix(drive): refuse two readiness retirements in one batch
DCG-Claude Sep 30, 2026
0c2e8e8
fix(dpp): decode readiness records with the trusted and untrusted dec…
DCG-Claude Sep 30, 2026
9ed45d1
test(drive): describe the readiness trees in the structure registry
DCG-Claude Sep 30, 2026
b70a33d
docs(platform): note that drive version 10 keeps the verify table
DCG-Claude Sep 30, 2026
beab4c6
fix(drive): refuse two readiness retirements in one batch in apply ge…
DCG-Claude Sep 30, 2026
fbebb06
fix(drive-abci): accept the unchecked insert the readiness helper now…
DCG-Claude Sep 30, 2026
0951e33
fix(drive): price the per-report walk when estimating readiness pruning
DCG-Claude Sep 30, 2026
9efc232
fix(drive): refuse readiness writes a batch would overwrite
DCG-Claude Sep 30, 2026
b382897
fix(drive): refuse early activation, reopening a retired round and un…
DCG-Claude Sep 30, 2026
acc6bc0
fix(dpp): keep readiness round and cursor invariants in the models
DCG-Claude Sep 30, 2026
a98d151
test(drive-abci): compare every level of the readiness subtrees acros…
DCG-Claude Sep 30, 2026
7ecfbc6
fix(drive): refuse raw grovedb writes beside readiness settlements an…
DCG-Claude Sep 30, 2026
793d751
fix(dpp): refuse readiness scan pages that repeat or rewind the position
DCG-Claude Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
- [Ranked Index Examples](drive/ranked-index-examples.md)
- [Time-Range Index TTL](drive/time-range-ttl.md)
- [Index-Only Document Types](drive/index-only-document-types.md)
- [Compilation Readiness](drive/compilation-readiness.md)

# Testing

Expand Down
88 changes: 88 additions & 0 deletions book/src/drive/compilation-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Compilation Readiness

A smart contract's executable bundle does not run the moment it is accepted. Every evonode first prepares it (validates, links and compiles it locally), and the bundle activates only once enough of the network has reported that it is ready. This chapter describes the state Drive keeps for that mechanism from protocol version 17: the rounds, the reports, the cursors, the deadlines and the funds. The block event that evaluates rounds, the state transition that carries a report, the fee schedule and the classification of local failures arrive in later parts and get their own sections here.

## Why rounds are stored under their own key

The confirmed policy has three rules that shape the layout:

- One pending executable version per contract. A replacement atomically cancels the previous round and its activation timer, and reports for the old bundle cannot count for the new one.
- No automatic expiry. A round that never gathers enough reports stays pending forever.
- Bounded work per block. Every step the network takes for readiness (accepting a report, validating reporters against membership, activating, cleaning up) has a fixed cap.

A round can accumulate one report per evonode of the network, and nothing bounds that number except churn. If a replacement deleted the old round's reports in the same batch, the cost of a replacement would grow with the number of reports, and the pinned GroveDB prices a recursive subtree delete by its contents while its average-case estimator prices only the tree element. So a round lives under its own key beneath its contract, and the contract holds a pointer to the current round. Replacing a round is a pointer swap plus a fixed number of inserts and deletes; the old subtree stays on disk, unreachable through the pointer, until a bounded cleanup step drains it over as many blocks as it needs.

## Layout

Everything lives under the existing `Votes` root (`112`) in a new child `r`, and the funds beside the voting funds under the prefunded specialized balances root (`40`):

```text
112 Votes
└─ r Readiness (NormalTree)
├─ 0 contracts (NormalTree) key: contract_id (32)
│ └─ contract_id (NormalTree)
│ ├─ c current round pointer Item: round_id (32)
│ └─ round_id (NormalTree, one per open round; normally exactly one)
│ ├─ 0 round record Item: ReadinessRound (versioned, serialized)
│ ├─ 1 reports CountTree; key: pro_tx_hash (32) -> Item: ReadinessReportRecord
│ └─ 2 scan cursor Item: ReadinessScanCursor (absent when no walk is open)
├─ 1 deadlines (NormalTree) key: encode_u64(deadline_ms) (NormalTree)
│ └─ contract_id (32) -> Item: round_id (32)
├─ 2 evaluation cursor Item: last contract_id visited by the block event
└─ 3 retired rounds awaiting cleanup (NormalTree)
key: round_id (32) -> Item: contract_id (32)
40 PreFundedSpecializedBalances (SumTree)
├─ 128 voting funds (existing)
└─ 129 readiness funds (SumTree) key: fund_id (32) -> SumItem
```

The keys `r`, `0` to `3`, `c` and `129` are provisional: the allocation register leaves new inner tags unallocated, and they are revised, if at all, before any network is asked to propose protocol version 17.

Paths and constants are in `packages/rs-drive/src/drive/votes/paths.rs`; the readiness fund key and paths are in `packages/rs-drive/src/drive/prefunded_specialized_balances/mod.rs`.

## The models

Defined in `packages/rs-dpp/src/voting/readiness/`, all versioned enums serialized with the platform serializer:

- `ReadinessRound` carries the contract, the round id, the bundle digest, the contract version the bundle activates, the preparation profile, when the bundle was accepted (committed block time and height), the status (`Pending` or `Crossed` with the crossing time and the activation deadline), the last evaluation mark (the core height of the membership view and the raw report count the round was last judged against), whether the fund ran short, the fund id and the payer. The round id is `hash_double(network_magic || contract_id || version || bundle_digest || accepted_at_height)`, so a replacement of the same bundle in a later block is a different round and a report signed for one network means nothing on another.
- `ReadinessReportRecord` is what is stored for one accepted report: the height it was accepted at and the profile it was compiled against. The count tree's own count is the raw number of distinct reporters; the record is there so the block event can judge a report later without the transition.
- `ReadinessScanCursor` is the persisted position of a paged walk over a round's reports, bound to the membership view (core height and eligible count) the walk started under.
- `ReadinessPayer` is the party the unused fund is refunded to. Only an identity can pay today; the contract-bucket owner is appended when typed storage flags land.

## Rounds

Opening a round (`open_readiness_round_operations`) writes the pointer, creates the round tree with its record and an empty reports count tree, creates the fund, and when a round was current retires it. Everything happens in one batch that the pinned GroveDB's consistency check accepts: every operation is on a distinct path and key, and no insert sits below a delete. An opening whose round id already has a tree under the contract is refused, whether that round is current or retired and awaiting cleanup: recreating the tree would leave the new round queued for cleanup.

Retiring a round (`retire_readiness_round_operations`, shared by replacement, cancellation and activation) never opens the reports tree. It queues the round under `[112, r, 3]`, drops the deadline entry when the round had crossed (with its per-time tree when no other round shares that time), empties the fund, charges the cleanup reserve to the epoch's processing pool and reports the remainder owed to the payer. The refund is returned rather than written because one batch may both refund the old payer and debit the new funding on the same identity, and two absolute balance writes on one key in one batch collapse; when the same identity funded both rounds the opening nets them into a single write, and otherwise it debits the new payer the full funding and credits the old payer its own refund. For the same reason the pool credit is an absolute rewrite of the epoch's processing pool item, so one retirement per applied batch is the contract of the helper.

An estimate reads no state, so opening and cancellation price the largest shape they can meet: a crossed round with a deadline entry and a time tree to remove, a fund to settle, the whole cleanup reserve credited to the pool, and a refund written to the retired round's payer as its own balance write.

Cancellation (`cancel_readiness_round_operations`) deletes the pointer and retires the round. Activation (`activate_readiness_round_operations`) does the same for a round that has crossed, is still the contract's current round and whose deadline the block has reached; routing the activated bundle into the contract's method tables is the caller's hook and lives outside Drive. In both cases the remainder is credited to the payer as unused preparation funding. That credit can repay the payer's debt, and only `apply_drive_operations` routes a repaid debt to the processing pool, so a block applies all three settlements as `ReadinessOperationType` batch operations (`OpenRound`, `CancelRound`, `ActivateRound`). It refuses a batch holding two of them, or one beside another identity balance or readiness fund write, which the absolute settlement writes would overwrite. A settlement or readiness fund write also shares its batch with no raw GroveDB operation, since Drive cannot tell which balances a raw write touches.

## Reports

`insert_readiness_report_operations` inserts one report into the current round's count tree if absent and says whether it was new. A retransmitted report is not new and writes nothing, so the count tree's count is the number of distinct reporters. The count is not an eligibility proof: the block event validates the reporters against the block's membership view before a crossing, and `prune_readiness_reports_operations` deletes the ones found ineligible.

`fetch_readiness_reports_page_operations` returns reports in key order, continuing after a given key, so the walk can be paged across blocks; `fetch_readiness_round_raw_count_operations` reads the count tree's count.

## Cursors, crossings and deadlines

A paged walk that does not finish in one block persists a `ReadinessScanCursor` under the round; a block whose membership view differs from the cursor's discards it and restarts, so a crossing is only ever committed from a walk completed against one coherent view. `update_readiness_round_evaluation_operations` rewrites the record with the last evaluation mark; a round whose mark matches the block's view and raw count is skipped at the cost of one read, and one whose mark differs is reconsidered, which is what makes a membership change reconsider every round even when the backlog spans several blocks.

`record_readiness_crossing_operations` marks the round crossed at the block's committed time with a deadline of `crossing_ms + clamp(crossing_ms - accepted_at_ms, 2 minutes, 1 hour)` (the bounds are `readiness_additional_wait_min_ms` and `readiness_additional_wait_max_ms` in `SystemLimits`) and queues the deadline under `[112, r, 1, encode_u64(deadline)]`, the same shape as the contested vote poll end-date queue. `fetch_readiness_rounds_due_operations` reads the entries at or before a time, oldest first; an entry whose round id is no longer the contract's current round is stale (the round was replaced or cancelled during the wait) and is dropped without activation. Retirement removes a crossed round's entry, and the per-time tree once it is empty: the due query walks the time keys under a limit and an empty time tree still spends it, so emptied trees ahead of a live deadline would hide that deadline from every block.

## Cleanup

`cleanup_retired_readiness_round_operations` runs one bounded step over the first queued retired round: it deletes up to a caller-given number of reports through the limited path-query delete and, once none remain, the record, the cursor and the now-empty count tree, then the round tree and, when no live round remains, the contract tree, and finally the queue entry. A round with more reports than the bound drains over several steps. The retired subtree has been unreachable since retirement, so the deferred deletion is invisible to every consensus rule; it only reclaims storage, paid by the cleanup reserve charged at retirement.

## Funds

A readiness fund is one sum item per round under `[40, 129]`, keyed by `hash_double("dashvm-readiness-fund-v1" || round_id)`. The five methods (`add_readiness_fund_operations`, `deduct_from_readiness_fund_operations`, `empty_readiness_fund_operations`, `fetch_readiness_fund`, `prove_readiness_fund`) are copies of their voting siblings on `[40, 128]`; the two families stay separate rather than sharing a path flag so the shipped voting generations remain byte-identical. Deductions keep a reserve untouchable so a round can always pay for its own deferred cleanup. Because the root `40` is a sum tree, readiness funds are inside the credit conservation check for free.

## Genesis and upgrade

`add_initial_vote_tree_main_structure_operations` generation 1 creates `[112, r]` with its children and `[40, 129]` at genesis. It is the vote setup that creates the fund tree, not the prefunded balances helper, because that helper is unversioned and genesis builds every lower layer in one batch. On a chain upgrading to protocol version 17, generation 3 of the protocol change hook creates the same elements with insert-if-not-exists through the same helper, and a test pins that a node born at 17 and a node upgraded from 16 hold byte-identical subtrees.

## Proofs

Three verifiers under `packages/rs-drive/src/verify/voting/` compile with the `verify` feature alone: `verify_readiness_round` (the pointer, the record and the raw count, from a merged proof the prover builds with the same queries), `verify_readiness_report` (one report by contract, round and reporter) and `verify_readiness_fund`. A query endpoint over them is client work and arrives separately.
22 changes: 22 additions & 0 deletions book/src/versioning/platform-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@ 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 field the incoming
version changes flows into them automatically, except the fields a later
version overrides explicitly: `PLATFORM_V17` names its own `drive` table, so a
Drive change arriving with the real 15 or 16 must be reconciled into that table
by hand in the same merge.

Protocol version 17 also introduces the compilation readiness storage (see
[Compilation Readiness](../drive/compilation-readiness.md)): `DRIVE_VERSION_V10`
turns on the `readiness` group of `DRIVE_VOTE_METHOD_VERSIONS_V4` (the three
readiness verifiers are generation 0 in every verify table),
`DRIVE_ABCI_METHOD_VERSIONS_V11` selects the protocol change hook generation
that creates the structures on upgrade, and
`SYSTEM_LIMITS_V5` carries the activation wait bounds.

## What a Version Snapshot Looks Like

Here is the very first version, `PLATFORM_V1`, slightly abbreviated:
Expand Down
1 change: 1 addition & 0 deletions packages/rs-dpp/src/voting/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod contender_structs;
pub mod readiness;
pub mod vote_choices;
pub mod vote_info_storage;
pub mod vote_polls;
Expand Down
14 changes: 14 additions & 0 deletions packages/rs-dpp/src/voting/readiness/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Compilation readiness: the state Platform keeps while the evonodes of the network prepare a
//! contract's executable bundle before it can be activated.
//!
//! A [`round::ReadinessRound`] is opened per contract when a bundle is accepted; evonodes send
//! signed readiness reports that Drive stores as [`report_record::ReadinessReportRecord`]
//! entries under the round's count tree; the block event validates the reporters against the
//! block's membership view in pages whose position is a [`scan_cursor::ReadinessScanCursor`],
//! and records the crossing and the activation deadline on the round. The
//! [`payer::ReadinessPayer`] is the party the unused fund is refunded to when the round retires.

pub mod payer;
pub mod report_record;
pub mod round;
pub mod scan_cursor;
32 changes: 32 additions & 0 deletions packages/rs-dpp/src/voting/readiness/payer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use bincode::{Decode, DecodeUntrusted, Encode};
use platform_value::Identifier;
use std::fmt;

/// The party that funded a compilation readiness round and receives its unused remainder when
/// the round retires (replacement, cancellation or activation).
///
/// Only an identity can pay today. The contract bucket variant that the typed storage flags
/// work introduces is appended when it lands; the enum is positional on the wire, so variants
/// are only ever added at the end.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
pub enum ReadinessPayer {
/// An identity funded the round; the refund is an identity balance credit.
Identity(Identifier),
}

impl ReadinessPayer {
/// The identity that receives the refund, when the payer is an identity.
pub fn identity_id(&self) -> Option<Identifier> {
match self {
ReadinessPayer::Identity(identity_id) => Some(*identity_id),
}
}
}

impl fmt::Display for ReadinessPayer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadinessPayer::Identity(identity_id) => write!(f, "Identity({})", identity_id),
}
}
}
Loading
Loading