feat(platform)!: document transitions state the action fee they agree to pay - #4858
Conversation
… to pay A document type's action fees are read off the contract when the action executes, not when the transition was signed, so a transition that said nothing would pay whatever the contract declares by then. Once a contract owner may change the fees, the users would not know. Version 2 of the document base transition, the default from protocol version 14, carries an action fee agreement: the owner and moderators amounts the signer saw declared, which must match the document type's exactly, each pot on its own, and for a fee priced by the fee multiplier the multiplier the signer knew with the increase, in percent, they accept, so a transition signed just before an epoch boundary is not refused for a small move. A fixed fee's agreement names no multiplier, and an agreement to the other pricing is a mismatch. The agreement is required whenever the document type charges a fee for the action, whoever pays the gas. Batch advanced structure validation 1 judges it off the action alone: the base action carries the declaration beside the agreement and the batch action the multiplier its transformer read. A refusal is a paid nonce bump that charges no fee: DocumentActionFeeAgreementNotSetError (40131), DocumentActionFeeAgreementMismatchError (40132), DocumentActionFeeMultiplierNotToleratedError (40133). An agreement on an action that charges nothing is ignored. A version 2 base cannot decode on earlier software, so the batch's basic structure validation refuses it while an earlier protocol version is active. The shipped serialization tables cap the base at 0 beside a default of 1 because nothing ever read those bounds, so only version 2 on is judged against them. Builders take the agreement through StateTransitionCreationOptions, which every document builder and the rs-sdk already thread; the six rs-sdk document builders gain with_action_fee_agreement. wasm-dpp2 gains the DocumentActionFeeAgreement class and builds a version 2 base only when an agreement is supplied, since its constructors have no platform version and a version 2 base is refused on a network still at protocol version 13. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tate-fee-disclosure-a4281b # Conflicts: # packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_fee_claim/tests.rs
|
Warning Review limit reachedNext included review available in 4 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (66)
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-20T16:48:58.242Z |
PR HygieneState: waiting-bots · commit
Self-review is an author attestation that you have read the diff: This check passes when the policy is satisfied; the repository decides whether merging requires it. |
|
🕓 Queued for automated review — 5th in line, estimated start in ~2.7 h (commit 26c88f7)
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v4.2-dev #4858 +/- ##
=============================================
- Coverage 85.81% 75.76% -10.05%
=============================================
Files 3102 3123 +21
Lines 410158 456510 +46352
=============================================
- Hits 351993 345894 -6099
- Misses 58165 110616 +52451
🚀 New features to boost your workflow:
|
…tate-fee-disclosure-a4281b # Conflicts: # packages/rs-dpp/src/errors/consensus/codes.rs # packages/rs-dpp/src/errors/consensus/state/state_error.rs
…ht the agreement before the nonce Two review findings on the action fee agreement. The refusal of a version 2 document base before protocol version 14 sat in the batch's shipped basic structure validator v0, as a version-table check inside a frozen generation. It moves to StateTransition::active_version_range, the mechanism this protocol version already uses for formats earlier software cannot decode (version 1 keys, a version 2 contract config, DataContractCreate version 1): a batch carrying such a base is inactive before protocol version 14 and is refused at decode, charging nothing, exactly as a binary that cannot decode it does. The v0 validator is byte-identical to the base branch again. A validator v1 selected by protocol version 14 would not have done the job: the gate has to act while protocol version 13 is active, where the tables select v0. The rs-sdk document builders reserve an identity contract nonce before they build the transition, and applying an agreement to a base too old to carry it fails locally after that, leaving the cached nonce ahead of Platform's with nothing broadcast. StateTransitionCreationOptions gains validate_base_carries_action_fee_agreement, which the six builders and the four put, transfer, purchase and update price paths call before the reservation, as the delete builder already does for its own checks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tate-fee-disclosure-a4281b # Conflicts: # packages/rs-dpp/src/errors/consensus/state/state_error.rs
…eletion-window #4858 appended its three action fee agreement errors to `StateError` first, so `DocumentModerationWindowElapsedError` follows them (discriminant 134), and its changelog item 20 sits after the window's lines in item 19. Also says, where the docs said "nobody removes it", that the window binds the moderators and the contract owner and not a document's own owner, and that the deadline is inclusive (review of #4864). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Issue being fixed or feature implemented
Follow-up to #4851. A document type's
actionFeesare read off the contract when the action executes, not when the transition was signed, so a transition that says nothing pays whatever the contract declares by then. Today that is safe only because a contract update may not change the fees of an existing document type. Once a contract owner is allowed to change them, the owner could change a fee and the users would not know what they are about to pay.This makes document transitions say what they agree to pay, so that lifting the "fees never change" rule later cannot make a signed transition pay a fee its signer never saw.
What was done?
The action fee agreement (
rs-dpp). Version 2 of the document base transition, the default from protocol version 14, carries$actionFeeAgreement:ownerandmoderatorsare the amounts the document type declares for the action, before any multiplier. They must match exactly, each pot on its own: a fee that was raised, lowered, or moved between the pots refuses the action.feeMultiplieris named for a fee priced by the fee multiplier and left out for afixedone, so the agreement also says how the fee is priced; an agreement to the other pricing is a mismatch.knownPermilleis the multiplier the signer priced the fee with andincreaseTolerancePercenthow far above it the executing epoch's multiplier may be, in percent of the known one (20 accepts up to 1.2 times). This is what deals with epoch boundaries. A multiplier that fell is always accepted. What is charged follows the epoch's multiplier, never the known one.DocumentActionFeeAgreement(versioned,V0),AgreedFeeMultiplier::tolerates(u128 math, no overflow at the largest terms),matches_declared,for_declared_fee,for_document_type_action(document_type, action, fee_multiplier), andDocumentActionFees::action_fee(action).The wire gate. A version 2 base cannot decode on 4.1 software, so a batch carrying one is inactive before protocol version 14 (
StateTransition::active_version_range): it is refused at decode, charging nothing, exactly as a binary that cannot decode it does. This is the mechanism this protocol version already uses for version 1 keys, a version 2 contract config andDataContractCreateversion 1, and it leaves the shipped batch validators untouched.STATE_TRANSITION_SERIALIZATION_VERSIONS_V3now says min 0, max 2, default 2 fordocument_base_state_transition, which is what makes the builders produce version 2.The check (
rs-drive,rs-drive-abci). The base action carries the declaration beside the agreement (DeclaredDocumentActionFee), the batch action already carries the multiplier its transformer read, andBatchTransitionAction::validate_action_fee_agreementsjudges them. It is called from batch advanced structure validation 1 (protocol version 14 only), right after the gas payer check, so it reads no state and the mempool refuses the same transitions. A refusal is a paid nonce bump that charges no fee:DocumentActionFeeAgreementNotSetErrorDocumentActionFeeAgreementMismatchErrorDocumentActionFeeMultiplierNotToleratedErrorStateErrordiscriminants 131 to 133, appended and pinned (40131 and 129 went to #4860, 130 to #4857).Builders and clients. The agreement reaches the builders through
StateTransitionCreationOptions.action_fee_agreement, which every document builder and the rs-sdk already thread (the builders have 295 callers in the drive-abci tests, so a new parameter was not an option). A base too old to carry the agreement is an error rather than a silent drop, andStateTransitionCreationOptions::validate_base_carries_action_fee_agreementlets a caller find that out before it reserves an identity contract nonce: the six rs-sdk builders and the fourput/transfer/purchase/update_pricepaths call it first, as the delete builder already does for its own checks, so a local failure cannot leave the cached nonce ahead of Platform's. The six rs-sdk document builders gainwith_action_fee_agreement. The rs-sdk does not fill the agreement in on its own: it must come from the contract the application showed its user, not from one fetched at signing time.wasm-dpp2gains theDocumentActionFeeAgreementclass andactionFeeAgreement?on the base and the six document transition option bags; its constructors have no platform version, so it builds a version 2 base only when an agreement is supplied and version 1 otherwise (an unconditional version 2 would be refused on a network still at protocol version 13).wasm-dppmaps the three errors. The threers-sdk-ffioption literals setaction_fee_agreement: None.Docs.
book/src/fees/overview.md(new "The transition agrees to the fee" section, and the "amounts never change" section reworded), the error codes table, entry 20 of thev14.rschangelog, and theactionFeesdescription of the v3 document meta-schema.Moderator document deletion (#4857) is its own state transition and charges no action fee, so it carries no agreement.
Not in this PR: wasm-sdk and js-evo-sdk methods and FFI / Swift / Kotlin exposure of the agreement (those clients cannot act on a fee-charging document type until they land; no existing contract declares fees), and lifting
validate_action_fees_unchanged(what actually lets an owner change a fee).RecheckTxdoes not rerun the agreement check, which is harmless while fees and the multiplier cannot change under a transition sitting in the mempool, and has to be added before either can.How Has This Been Tested?
rs-dpp: unit tests for the tolerance math (boundary, lower multiplier, zero tolerance, largest terms), exact matching of amounts and pricing, the JSON round trip, the version 2 base being inactive before protocol version 14 (active range, refusal at decode at 13, round trip at latest), the nonce preflight, and the pinnedStateErrordiscriminants. Fullcargo test -p dpp --all-features --lib: 4,460 passed.rs-drive-abci, throughprocess_raw_state_transitionsandcheck_tx: no agreement (both pricings), other amounts for either pot (higher, lower, swapped), the other pricing, a multiplier that rose within the tolerance (1200 on 1000 + 20%) and beyond it (1201), a multiplier that fell, a sponsored transition without an agreement, an agreement on an action that charges nothing, and a version 2 base at protocol version 13 (the batch is not active, nothing is charged). The existing fee tests now carry agreements, including the six-action test. Fullcargo test -p drive-abci --libwas run: one pre-existing fee claim test created a paid document without an agreement and was fixed; everything else passed, and no pinned fee baseline moved from the version 2 default.rs-drive:state_transition_action,verify::state_transition,prove_state_transition: 1,233 passed.cargo clippy -- -D warningson dpp, drive, drive-abci, dash-sdk, platform-version (all targets), rs-sdk-ffi, wasm-dpp, and wasm-dpp2 (wasm32-unknown-unknown);cargo fmt --all --check;cargo check --workspace --all-targets.cargo check --workspace --all-targets,cargo fmt --all --check, clippy-D warningson the crates above and wasm-dpp2, dpp state transition / batch / action fee / consensus tests (1,187), the drive-abci batch and fee claim tests (520), and the drivestate_transition_actiontests (1,156) all pass.wasm-dpp2TypeScript specs (they need a wasm build), and the strategy tests.Breaking Changes
Consensus-breaking, protocol version 14 only (unreleased):
$actionFeeAgreement(40132 to 40134). Only contracts on the v3 document meta-schema can declareactionFees, so no existing contract is affected.$baseFormatVersion: "2"), which earlier protocol versions refuse.StateTransitionCreationOptionsgains theaction_fee_agreementfield (struct literals need it),DocumentBaseTransitiongains theV2variant,StateErrorgains three variants, andDocumentBaseTransitionActionV0::declared_action_feenow boxes aDeclaredDocumentActionFee.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code