feat(platform)!: add family-specific state transition size limits and the large contract envelope decode path - #4717
feat(platform)!: add family-specific state transition size limits and the large contract envelope decode path#4717DCG-Claude wants to merge 9 commits into
Conversation
…ameters to the version tables Introduce protocol versions 15 to 17 in the shape the smart-contract computation limits branch uses (15 and 16 as struct-update placeholders, 17 as the 5.0 version) so the two branches merge as a union. SystemLimits gains the contract-code envelope, decode budget, bundle byte and module count limits, None on every shipped table and the mocks and provisional register values on SYSTEM_LIMITS_V5. ConsensusVersions gains the Tenderdash block byte and gas caps Drive pushes at activation, None before 17. DRIVE_ABCI_METHOD_VERSIONS_V11 selects the family-cap decode generation and the block parameter push; DRIVE_ABCI_QUERY_VERSIONS_V4 selects the proof query that decodes under the family budget. The implementations follow in later commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d decode under its budget StateTransition::peek_envelope_kind reads at most ten bytes (the outer and inner bincode variant indices) and names the contract-code capable generations of the contract create and update transitions without decoding or allocating; anything else is Ordinary and bounded exactly as today. family_max_size and family_decode_budget read the cap and budget from the version tables, deserialize_from_bytes_with_budget applies the fixed set of bincode budgets the tables may hold, and deserialize_from_bytes_in_version_bounded is the entry point of the paths that admit large envelopes: it decodes under the family budget and reports a not-active variant as the consensus error it is instead of a protocol error. The untouched deserialize_from_bytes_in_version keeps its behaviour for the frozen v0 block decoder. Finding recorded in the tests: the limit attribute on the enum is not read by the derive, so the shipped decode has no bincode budget and the ordinary families are bounded by the wire cap alone. That decode is kept as is; the contract-code envelope is the first family decoded under an explicit budget. StateTransitionFamilyMaxSizeExceededError (code 10604) names the family whose cap a large envelope exceeded, with the wasm-dpp mapping arm. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… and push block parameters decode_raw_state_transitions v1 compares the raw length with the cap of the family the wire prefix names, decodes under that family's budget and files a variant the active version does not admit as an unpaid consensus rejection instead of a node fault. Version 0 stays byte-identical and its tests pin protocol version 16, the last one whose tables select it. consensus_params_update v2 pushes ConsensusParams.block when the new protocol version sets the block byte and gas caps and the previous one did not carry the same pair, on top of the version parameters v1 pushes, so every validator raises the Tenderdash block size at the same height and a fresh network gets it from init_chain. query_proofs v1 applies the same family cap and budget to the transition of a getProofs request. The Drive gRPC server caps decoding and encoding at 34 MiB on the Platform, DriveInternal and ABCI CheckTx services, above every family cap of every registered version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… cap The Platform service decodes requests up to 34 MiB, the broadcast pre-filter reads the family from the wire prefix and applies the cap of that family, and the client to Drive encodes up to 34 MiB so a waitForStateTransitionResult proof request carrying a maximal transition reaches Drive; a local server test pins both sides of that boundary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ade on both proposal paths The block mimic keeps the consensus parameter update of prepare_proposal and, when process_proposal runs independently, of that path as well, and the strategy harness records both per height. A seeded upgrade from 16 to 17 with independent process-proposal verification asserts that the boundary block pushes the block byte and gas caps from both paths, every other block pushes none, and a retried round after activation is served from the cached proposer results without a push. Two mimic fixes the test needed: the validator path's response status is asserted instead of assuming a block execution context, and the genesis rewind to the init-chain savepoint is gated on the genesis height rather than on an open transaction, which a retried round of any block also has. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…th limits for contract-code envelopes max-tx-bytes and max-body-bytes were hard-coded in the Tenderdash template; they become the options platform.drive.tenderdash.mempool.maxTxBytes (32 MiB, the contract-code state transition cap) and platform.drive.tenderdash.rpc.maxBodyBytes (50,000,000: the base64 inflation of such a transition inside broadcast_tx_sync plus the JSON envelope). The stock p2p send and receive rates rise to 20,480,000 bytes per second so a maximal transaction gossips in seconds. A 5.0.0 migration adds the options and moves only stock bandwidth caps; operator-tuned values are left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… budgets and block parameter push Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ents Tenderdash writes every event as one uncompressed WebSocket frame, so tungstenite's default 16 MiB frame limit disconnected the listener on the Tx event of a contract-code state transition at the 32 MiB family cap (44.7 MB of base64) and on the NewBlock event of a full 36 MiB block, exactly when waitForStateTransitionResult was waiting for the transaction. Both connections now use an explicit 64 MiB message and frame limit. A test pins the cap above the base64 size of the largest block of every registered protocol version, and two tests deliver a family-cap Tx event and a block-cap NewBlock event through a local server: the configured connection receives both, the default one is refused at the frame limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…block parameter push The harness now records every round of every block with the consensus parameter update of both proposal paths and the app hash the round produced. The upgrade test runs the chain to the block before the activation of 17, proposes the activation block in round 0, abandons it without finalization, proposes it again in round 1 and finalizes that: both rounds return the same block parameters from prepare_proposal and process_proposal over the same app hash, the committed state is the finalized round's (app hash and GroveDB root agree), and every block before the activation and every round of the blocks after it pushes none. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-12T20:41:48.262Z |
|
✅ Final review complete — no blockers (commit 7a257f1) · triage: critical |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v5.0-dev #4717 +/- ##
============================================
- Coverage 86.36% 84.92% -1.44%
============================================
Files 2766 2770 +4
Lines 366105 373168 +7063
============================================
+ Hits 316191 316921 +730
- Misses 49914 56247 +6333
🚀 New features to boost your workflow:
|
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
The new bounded decoder has one confirmed diagnostic error: it reports a byte-valued budget as kilobytes. Source inspection did not establish an in-scope blocker among the remaining findings; the proposed activation collision is historical, and the transport concerns do not demonstrate a failure under the current implementation and deployment controls.
🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: security-auditor); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 5: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)
- Triage:
criticalbygpt-6-astra(effort low) — This large, intricate diff changes consensus rules in rs-drive-abci's decode_raw_state_transitions/v1 and consensus_params_update/v2, plus peer-facing deserialization in rs-dpp's envelope_kind.rs, introducing version-gated family size limits, decode budgets, and block parameter updates whose consistency across ingress and upgrade paths is consensus-critical. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— security-auditor (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 11% left, 5h 100% left),glm-5.3-flash(zai below 15% reserve: 5h 99% left, weekly 13% left) - Fresh verifier:
gpt-6-astra— final-verifier; agentastra-verifier - Phase 2 reviewers:
gpt-6-astra— general (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6-astra— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-dpp/src/state_transition/envelope_kind.rs`:
- [SUGGESTION] packages/rs-dpp/src/state_transition/envelope_kind.rs:198-203: Convert the bounded decode budget to kilobytes in the error
`BUDGET` is 67,108,864 bytes, but `ProtocolError::MaxEncodedBytesReachedError` formats `max_size_kbytes` as `Payload reached a {max_size_kbytes}KB limit`. A limit hit therefore reports 67,108,864 KB instead of 65,536 KB, overstating the configured budget by 1,024 times. The new v1 block decoder also converts this error to a client-visible `SerializedObjectParsingError`. Set `max_size_kbytes` to `BUDGET / 1024`, leave `size_hit` in bytes, and extend the crafted-length-claim test to assert the reported budget rather than only the error variant.
| .map_err(|error| match error { | ||
| DecodeError::Io { .. } | DecodeError::LimitExceeded => { | ||
| ProtocolError::MaxEncodedBytesReachedError { | ||
| max_size_kbytes: BUDGET, | ||
| size_hit: bytes.len(), | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: Convert the bounded decode budget to kilobytes in the error
BUDGET is 67,108,864 bytes, but ProtocolError::MaxEncodedBytesReachedError formats max_size_kbytes as Payload reached a {max_size_kbytes}KB limit. A limit hit therefore reports 67,108,864 KB instead of 65,536 KB, overstating the configured budget by 1,024 times. The new v1 block decoder also converts this error to a client-visible SerializedObjectParsingError. Set max_size_kbytes to BUDGET / 1024, leave size_hit in bytes, and extend the crafted-length-claim test to assert the reported budget rather than only the error variant.
source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality, security-auditor); gpt-6-astra (phase2-reviewer: general, rust-quality, security-auditor)
Issue being fixed or feature implemented
Part 1 of 2 for R06-05 of the smart-contract plan (#4626, workstream #4684).
The plan extends the contract create and update state transitions so they can carry large canonical code and declarations, with larger bounded limits applied consistently on every ingress path rather than a separate upload route. Today every state transition family shares one 20 KiB cap, the block decoder maps a not-yet-active variant to a node fault, the Tenderdash block size is a genesis value no protocol version can move, and the transport caps of rs-dapi and Drive would refuse a multi-megabyte transaction before Drive saw it.
This part lands the consensus-side scaffolding for the large envelopes without producing one yet: the version-table limits, the family detection from the wire prefix, the decode generation that applies the family cap and budget, the Tenderdash block parameter push, and the transport and node configuration sized for the cap. Part 2 adds the contract-code capable generation of the two transitions, the bundle type, its validation and its costs.
Refs #4684
What was done?
Version tables (
packages/rs-platform-version)v15.rsandv16.rsare struct-update placeholders,v17.rsis the 5.0 version,LATEST_VERSIONis 17. Whichever of the two branches lands second rebases; the shared files merge as a union.SystemLimitsgainsmax_contract_code_state_transition_size,max_contract_code_state_transition_decode_budget,max_contract_code_bundle_bytesandmax_contract_code_modules_per_bundle, allOption,NoneonSYSTEM_LIMITS_V1toV4and the mock tables, and set on the newSYSTEM_LIMITS_V5with a compile-time consistency assertion. The doc comment onmax_state_transition_sizenow states the relation to the Tenderdashmax-tx-bytescorrectly (at least the largest family cap, no longer equal).ConsensusVersionsgainsblock_max_bytesandblock_max_gas,Noneon every shipped version and the mocks, set on v17.DRIVE_ABCI_METHOD_VERSIONS_V11selectsdecode_raw_state_transitions1 andconsensus_params_update2;DRIVE_ABCI_QUERY_VERSIONS_V4selectsproofs_query1. Both are referenced byPLATFORM_V17only.dpp (
packages/rs-dpp/src/state_transition/envelope_kind.rs, new)StateTransition::peek_envelope_kindreads at most ten bytes (the outer and inner bincode variant indices) and returnsStateTransitionEnvelopeKind::OrdinaryorContractCodeCapable { family }for the contract create and update transitions in the generation that will carry a code bundle (outer index 0 or 1, inner index 1). Unknown or truncated prefixes areOrdinary. The discriminants are pinned against real serialized transitions so a reordering of either enum fails a test.family_max_sizeandfamily_decode_budgetread the cap and the budget from the tables, falling back to the ordinary values where the table holdsNone.deserialize_from_bytes_with_budgetdecodes underStateTransitionDecodeBudget::Historical(the shipped decode) orBounded(67_108_864); any other bounded value isCorruptedCodeExecution, unreachable while the tables hold that number and pinned over every registered version.deserialize_from_bytes_in_version_boundedis the entry point of the paths that admit large envelopes: it decodes under the family budget with the version's value depth limit and reports a not-active variant asBasicError::StateTransitionNotActiveErrorinsideProtocolError::ConsensusError. The existingdeserialize_from_bytes_in_versionis untouched.StateTransitionFamilyMaxSizeExceededError(code 10604) names the family whose cap a large envelope exceeded; ordinary families keepStateTransitionMaxSizeExceededError. wasm-dpp gets the mapping arm.drive-abci
execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1compares the raw length with the family cap before decoding, decodes through the bounded entry point, and files a not-active variant asInvalidEncoding(an unpaid consensus rejection) instead ofFailedToDecode(a node fault).v0is byte-identical; its tests pin protocol version 16, the last one whose tables select it.execution/engine/consensus_params_update/v2pushesConsensusParams.blockwhen the new protocol version sets both block caps and the previous version did not carry the same pair, on top of the version parameters v1 pushes.init_chaindiffs the first version against the genesis one, so a fresh 5.0 network gets the block size at genesis.query/proofs/v1applies the same family cap and budget to the transition of agetProofsrequest.server.rscaps decoding and encoding atMAX_GRPC_MESSAGE_BYTES(34 MiB) on the Platform, DriveInternal and ABCI CheckTx services; a test asserts the constant exceeds every family cap of every registered version. Tonic's 4 MiB default would have rejected a large CheckTx before Drive saw it.mimic/mod.rs:MimicExecuteBlockOutcomecarries the consensus parameter update of both proposal paths, and the strategy harness records every round of every block with its app hash (BlockProposalRounds). Two fixes the new strategy test needed: the validator path's response status is asserted (a rejection used to surface as a missing block execution context), and the genesis rewind to the init-chain savepoint is gated on the genesis height rather than on an open transaction, which a retried round of any block also has.rs-dapi
server/grpc.rs: the Platform service decodes up to 34 MiB.services/platform_service/broadcast_state_transition.rs: the pre-filter reads the family from the wire prefix and applies that family's cap fromPlatformVersion::latest()(a static upper bound; Drive enforces the active version's cap).clients/drive_client.rs: the client to Drive encodes up to 34 MiB, sincewaitForStateTransitionResultre-sends the whole transaction insideGetProofsRequest.clients/tenderdash_websocket.rs: the Tenderdash event listener connects with explicit tungstenite limits (MAX_TENDERDASH_WS_MESSAGE_BYTES, 64 MiB for both the message and the frame). Tenderdash writes every event as one uncompressed frame, and tungstenite's default 16 MiB frame limit would disconnect the listener on theTxevent of a family-cap transaction (44.7 MB of base64) or theNewBlockevent of a full 36 MiB block, exactly whenwaitForStateTransitionResultneeds it. A test pins the cap above the base64 size of the largest block of every registered version, and two tests deliver a family-capTxevent and a block-capNewBlockevent through a local server: the configured connection receives both, the default connection is refused at the frame limit.dashmate
platform.drive.tenderdash.mempool.maxTxBytes(default 33,554,432) andplatform.drive.tenderdash.rpc.maxBodyBytes(default 50,000,000) replace the hard-codedmax-tx-bytesandmax-body-bytesin the Tenderdash template; the stock p2psendRateandrecvRaterise to 20,480,000 bytes per second. A5.0.0migration adds the options and moves only stock bandwidth caps. Schema, docs and a migration unit test are updated. Genesisblock.max_bytesstays 2 MiB: Drive raises it at the protocol boundary.Book:
state-transitions/lifecycle.md(caps, budgets, the prefix peek, the block parameter push),versioning/feature-versions.mdandplatform-version.md(the new fields),error-handling/error-codes.md(10604).Finding recorded for FIX-06. The
#[platform_serialize(limit = 100000)]onStateTransitionis inert: the derive reads only the firstplatform_serializeattribute (unversioned), sodeserialize_from_bytesapplieswith_no_limit()and the ordinary families are bounded by the wire cap alone (a 5 MB payload decodes through it; the testshould_decode_a_large_real_payload_under_both_budgetsdocuments this). That decode is deliberately left as shipped, because changing it retroactively could change which historical blocks decode. The contract-code envelope is the first family decoded under an explicit bincode budget.StateTransitionDecodeBudget::Historicalnames the shipped behaviour so the distinction is visible at every call site.Benchmark and chunking decision. Micro-benchmark on this machine (Apple Silicon, release build, bincode 2.0.1, sha2 0.10), one module plus a 16 KiB schema, rounded:
A crafted 8 GB length claim in a 10 byte payload is rejected before allocation under the bounded budget. A 32 MiB envelope decodes in under 2 ms and hashes twice in about 110 ms, inside a CheckTx or block slot; Tenderdash's hard block cap is 100 MB and its ABCI socket reader accepts 100 MB, so a 32 MiB transaction needs configuration, not protocol work. Chunking would add staging state, expiry, cleanup fees and refunds the owner asked to specify only if measurements require it; they do not. The remaining cost is gossip bandwidth (about 1.6 s per hop at the new default rate), the one number the testnet rehearsal must confirm before the cap is final. The
#[ignore]testmeasure_decode_gate_at_the_family_capinenvelope_kind.rsreproduces the decode-gate half in-tree.Plan review finding R4 (use a hashing operation that can represent large envelopes). No hashing operation is recorded in this part. Part 2 widens
HashBlockCountfromu16tou32so aDoubleSha256operation over up to 32 MiB of signable bytes can be priced, records it in the transformer, and prices each module hash through the hashing fee group; the disposition is unchanged.Left for FIX-06. The family cap and the bincode budget stop over-limit input before allocation, but the per-module bound of part 2 is checked after the envelope is decoded into owned buffers. A zero-copy walk that checks each module length against the table before it is materialised, and the DAPI-side rejection of a bundle-carrying envelope before it reaches Tenderdash, are FIX-06's work.
Sibling branches. #4705 (R06-01) introduces the same
v15.rs,v16.rs,v17.rs,version/mod.rs,protocol_version.rsandsystem_limits/{mod,v5}.rsedits; whichever lands second rebases and takes the union (SYSTEM_LIMITS_V5carries both feature groups,PLATFORM_V17both change lists). #4648 (v4.2-dev) introduces aDRIVE_ABCI_METHOD_VERSIONS_V11on its own v15; on the forward merge the incoming generation keeps its number and this one is renumbered.How Has This Been Tested?
Local gate, exit codes captured (private
CARGO_TARGET_DIR):Tests added:
platform-version: the contract-code limits are absent below 17 and present from 17; the block parameters are absent below 17 and paired from 17 (both loopPLATFORM_VERSIONS, since the decoder's cap choice and the push depend on them).dppenvelope_kind: classification of every serialized fixture, the contract-code prefix of both families pinned against real V0 bytes, truncated and unknown prefixes, cap and budget selection on both sides of the gate, every table budget supported, an unsupported budget rejected, a crafted length claim rejected by the limit under the bounded budget, a compact wide-container transition decoding identically through the historical and the bounded path at 14 and at latest, not-active reported as a consensus error only on the bounded path.drive-abcidecode_raw_state_transitions/v1: ordinary cap kept for every ordinary family and for V0 contract transitions, a contract-code envelope one byte over the family cap rejected with 10604 and one at the cap reaching the decode step, not-active classified asInvalidEncoding; dispatcher tests run the same inputs through v0 (at 16) and v1 (at latest).consensus_params_update/v2: push when set, no push when unchanged or between versions that set none, never a zeroed gas cap, emergency updates keep priority, genesis push; dispatcher arm for 2.query_proofs_v1: at the family cap reaches decode, above it is rejected, ordinary cap kept.server.rs: the gRPC cap exceeds every family cap. Strategy testrun_chain_upgrade_to_v17_pushes_block_params_on_both_proposal_paths: seeded upgrade 16 to 17 with independent process-proposal verification and signed chain locks; the activation block itself is proposed in round 0, abandoned without finalization, and proposed again in round 1, and both rounds return the same block parameters fromprepare_proposalandprocess_proposalover the same app hash; the committed state is the finalized round's (app hash and GroveDB root agree, the abandoned round left nothing behind); every block before the activation and every round of the three blocks after it pushes none.rs-dapi: the broadcast pre-filter for both kinds and one byte over each cap; a local tonic server behind the configured client receives aGetProofsRequestcarrying a family-cap transaction intact and never sees one above the client's outbound cap; the WebSocket cap test and the two large-event delivery tests described above.dashmate:migrateConfigFileFactory.spec.jscovers the5.0.0migration (options added, stock rates raised, tuned rate kept, migrated config loads). The mocha suite in this checkout needs a builtwasm-dppthroughjs-dapi-client, which was not available locally; the same assertions plus the template rendering of all four default configs were run directly through the dashmate factories withyarn node(all pass), and CI runs the suite.Breaking Changes
This part is a consensus change at protocol version 17: at the activation boundary Drive pushes
ConsensusParams.block { max_bytes: 37_748_736, max_gas: 57_631_392_000 }to Tenderdash from both proposal paths (consensus_params_updatev2), so every validator raises the block size limit at the same height, and the block decoder of that version applies a 32 MiB cap to the contract-code capable contract transitions (decode_raw_state_transitionsv1). Nodes that disagree on either would fork at that height, which is what the!marks.Historical behaviour is preserved: every shipped
vNmodule is byte-identical, the new tables are read only at protocol version 17, which no network runs yet, and every family cap at every active version is unchanged. The dashmate config format moves to5.0.0with a migration that adds two required options. Part 2 adds the wire generation that can actually carry a code bundle and carries!as well.Decisions taken (provisional values):
33_554_432): register "Signed contract create/update transition".67_108_864): the wire cap plus headroom for the container claims bincode charges while decoding a contract schema.16_777_216): register "Canonical WASM 16 MiB per version", read as the bundle total.block.max_bytesat 5.0: 36 MiB (37_748_736), one maximal transaction plus 4 MiB for header, commit and evidence;block.max_gas57,631,392,000, the value every network launched with, carried so the push keeps it.max-tx-bytes33,554,432 andmax-body-bytes50,000,000 (base64 inflation of the RPC body): node-local dashmate defaults.All numbers are measured and revised before any network is asked to propose protocol version 17; the register in #4684 is the place they are tracked.
Checklist:
For repository code-owners and collaborators only
🤖 Posted autonomously by DashVM (Claude Fable 5.1) on behalf of pasta.
🤖 Generated with Claude Code
Automated reviewer consensus (Fable 5.1 implementer, GPT-6 Astra reviewer)
Reviewer consensus
Plan Review consensus
resolvedresolvedwithdrawnstill_openresolvedresolvedresolvedresolvedresolvedReview consensus
resolvedresolvedresolvedresolved