Skip to content

feat(platform)!: document transitions state the action fee they agree to pay - #4858

Merged
QuantumExplorer merged 5 commits into
v4.2-devfrom
claude/document-state-fee-disclosure-a4281b
Sep 20, 2026
Merged

QuantumExplorer merged 5 commits into
v4.2-devfrom
claude/document-state-fee-disclosure-a4281b

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Sep 20, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Follow-up to #4851. A document type's actionFees are 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:

"$actionFeeAgreement": {
  "$formatVersion": "0",
  "owner": 10000000,
  "moderators": 100000000,
  "feeMultiplier": { "knownPermille": 1000, "increaseTolerancePercent": 20 }
}
  • owner and moderators are 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.
  • feeMultiplier is named for a fee priced by the fee multiplier and left out for a fixed one, so the agreement also says how the fee is priced; an agreement to the other pricing is a mismatch. knownPermille is the multiplier the signer priced the fee with and increaseTolerancePercent how 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.
  • Required whenever the document type charges a fee for the action, whoever pays the gas (a preferred sponsor can hand the fee back to the signer). An agreement on an action that charges nothing is ignored.
  • New: 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), and DocumentActionFees::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 and DataContractCreate version 1, and it leaves the shipped batch validators untouched. STATE_TRANSITION_SERIALIZATION_VERSIONS_V3 now says min 0, max 2, default 2 for document_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, and BatchTransitionAction::validate_action_fee_agreements judges 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:

Code Error
40132 DocumentActionFeeAgreementNotSetError
40133 DocumentActionFeeAgreementMismatchError
40134 DocumentActionFeeMultiplierNotToleratedError

StateError discriminants 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, and StateTransitionCreationOptions::validate_base_carries_action_fee_agreement lets a caller find that out before it reserves an identity contract nonce: the six rs-sdk builders and the four put / transfer / purchase / update_price paths 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 gain with_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-dpp2 gains the DocumentActionFeeAgreement class and actionFeeAgreement? 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-dpp maps the three errors. The three rs-sdk-ffi option literals set action_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 the v14.rs changelog, and the actionFees description 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). RecheckTx does 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 pinned StateError discriminants. Full cargo test -p dpp --all-features --lib: 4,460 passed.
  • rs-drive-abci, through process_raw_state_transitions and check_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. Full cargo test -p drive-abci --lib was 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 warnings on 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.
  • After merging v4.2-dev (test(dpp): guard doctype keyword names against stray keys of contracts admitted under meta-schema v0 #4855, feat(platform)!: document ids commit to the identity contract nonce #4859, feat(platform)!: references to deletable documents (refersTo deletableDocument) #4860, then feat(platform)!: moderators delete documents of the document types that allow it #4857) and the review fixes: cargo check --workspace --all-targets, cargo fmt --all --check, clippy -D warnings on 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 drive state_transition_action tests (1,156) all pass.
  • Not run: the two wasm-dpp2 TypeScript specs (they need a wasm build), and the strategy tests.

Breaking Changes

Consensus-breaking, protocol version 14 only (unreleased):

  • A document transition on an action whose document type charges an action fee is refused without a matching $actionFeeAgreement (40132 to 40134). Only contracts on the v3 document meta-schema can declare actionFees, so no existing contract is affected.
  • From protocol version 14 the document builders produce version 2 of the document base transition ($baseFormatVersion: "2"), which earlier protocol versions refuse.
  • Rust API: StateTransitionCreationOptions gains the action_fee_agreement field (struct literals need it), DocumentBaseTransition gains the V2 variant, StateError gains three variants, and DocumentBaseTransitionActionV0::declared_action_fee now boxes a DeclaredDocumentActionFee.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

QuantumExplorer and others added 2 commits September 20, 2026 19:29
… 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
@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 4 minutes.

Check out review usage here.

View limit details

Limit 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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: aa1e83fd-0ed1-4e6a-a9ec-cf9cc46d4d98

📥 Commits

Reviewing files that changed from the base of the PR and between e24aa8a and 6b07659.

📒 Files selected for processing (66)
  • book/src/error-handling/error-codes.md
  • book/src/fees/overview.md
  • packages/rs-dpp/schema/meta_schemas/document/v3/document-meta.json
  • packages/rs-dpp/src/data_contract/document_type/action_fees/agreement/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/action_fees/agreement/v0/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/action_fees/mod.rs
  • packages/rs-dpp/src/data_contract/document_type/methods/validate_update/v1/mod.rs
  • packages/rs-dpp/src/errors/consensus/codes.rs
  • packages/rs-dpp/src/errors/consensus/state/document/document_action_fee_agreement_mismatch_error.rs
  • packages/rs-dpp/src/errors/consensus/state/document/document_action_fee_agreement_not_set_error.rs
  • packages/rs-dpp/src/errors/consensus/state/document/document_action_fee_multiplier_not_tolerated_error.rs
  • packages/rs-dpp/src/errors/consensus/state/document/mod.rs
  • packages/rs-dpp/src/errors/consensus/state/state_error.rs
  • packages/rs-dpp/src/state_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/fields.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/from_document.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/from_document.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/v2_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_create_transition/v0/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_index_only_delete_transition/v0/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_replace_transition/v0/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v0/v0_methods.rs
  • packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v0_methods.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/advanced_structure/v1/mod.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/action_fees.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/gas_sponsorship.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/contract_fee_claim/tests.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs
  • packages/rs-drive/src/state_transition_action/batch/mod.rs
  • packages/rs-drive/src/state_transition_action/batch/v0/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs
  • packages/rs-platform-version/src/version/v14.rs
  • packages/rs-sdk-ffi/src/document/helpers.rs
  • packages/rs-sdk-ffi/src/identity/helpers.rs
  • packages/rs-sdk-ffi/src/token/utils.rs
  • packages/rs-sdk/src/platform/documents/transitions/create.rs
  • packages/rs-sdk/src/platform/documents/transitions/delete.rs
  • packages/rs-sdk/src/platform/documents/transitions/purchase.rs
  • packages/rs-sdk/src/platform/documents/transitions/replace.rs
  • packages/rs-sdk/src/platform/documents/transitions/set_price.rs
  • packages/rs-sdk/src/platform/documents/transitions/transfer.rs
  • packages/rs-sdk/src/platform/transition/purchase_document.rs
  • packages/rs-sdk/src/platform/transition/put_document.rs
  • packages/rs-sdk/src/platform/transition/transfer_document.rs
  • packages/rs-sdk/src/platform/transition/update_price_of_document.rs
  • packages/wasm-dpp/src/errors/consensus/consensus_error.rs
  • packages/wasm-dpp2/src/state_transitions/batch/action_fee_agreement.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_base_transition.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/create.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/delete.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/purchase.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/replace.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/transfer.rs
  • packages/wasm-dpp2/src/state_transitions/batch/document_transitions/update_price.rs
  • packages/wasm-dpp2/src/state_transitions/batch/generators.rs
  • packages/wasm-dpp2/src/state_transitions/batch/mod.rs
  • packages/wasm-dpp2/tests/unit/DocumentActionFeeAgreement.spec.ts
  • packages/wasm-dpp2/tests/unit/DocumentsTransitions.spec.ts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 20, 2026
@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

📖 Book Preview built successfully.

Download the preview from the workflow artifacts.
To view locally: download the artifact, unzip, and open index.html.

Updated at 2026-09-20T16:48:58.242Z

@github-actions

github-actions Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

PR Hygiene

State: waiting-bots · commit 26c88f72780e28e8dfc5c39bf9795fd5f1e220f3

  • coderabbitai has not reported for the current head
  • thepastaclaw has not reported for the current head

Self-review is an author attestation that you have read the diff:
/self-reviewed — covers everything pushed so far; post it again after a new push.
/skip-bots — proceed without the bots that have not reported; anyone with write access may, and the report says who did.

This check passes when the policy is satisfied; the repository decides whether merging requires it.

@thepastaclaw

thepastaclaw commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

🕓 Queued for automated review — 5th in line, estimated start in ~2.7 h (commit 26c88f7)
Estimated review time once started: ~1.1 h (two-phase automated review; median of recent runs).

  • Request priority review — click to move this review to the front of the queue.

@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.12389% with 126 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.76%. Comparing base (b924b25) to head (6b07659).
⚠️ Report is 4 commits behind head on v4.2-dev.

Files with missing lines Patch % Lines
packages/rs-dpp/src/state_transition/mod.rs 62.06% 33 Missing ⚠️
...ansition/document_base_transition/v2/v2_methods.rs 47.05% 27 Missing ⚠️
..._transition/document_base_transition/v2_methods.rs 54.28% 16 Missing ⚠️
...-drive/src/state_transition_action/batch/v0/mod.rs 73.21% 15 Missing ⚠️
...src/data_contract/document_type/action_fees/mod.rs 47.36% 10 Missing ⚠️
...ct/document_type/methods/validate_update/v1/mod.rs 0.00% 6 Missing ⚠️
.../rs-drive/src/state_transition_action/batch/mod.rs 45.45% 6 Missing ⚠️
...ate_transitions/batch/advanced_structure/v1/mod.rs 73.68% 5 Missing ⚠️
.../document_base_transition_action/v0/transformer.rs 75.00% 3 Missing ⚠️
...ontract/document_type/action_fees/agreement/mod.rs 98.38% 2 Missing ⚠️
... and 3 more
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     
Components Coverage Δ
dpp 72.42% <65.23%> (-16.69%) ⬇️
drive 77.80% <73.56%> (-6.62%) ⬇️
drive-abci 76.81% <75.02%> (-9.63%) ⬇️
sdk ∅ <ø> (∅)
dapi-client ∅ <ø> (∅)
platform-version ∅ <ø> (∅)
platform-value 86.29% <ø> (-6.68%) ⬇️
platform-wallet ∅ <ø> (∅)
drive-proof-verifier 25.02% <0.61%> (-10.18%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@QuantumExplorer QuantumExplorer left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed

…tate-fee-disclosure-a4281b

# Conflicts:
#	packages/rs-dpp/src/errors/consensus/codes.rs
#	packages/rs-dpp/src/errors/consensus/state/state_error.rs
@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 20, 2026
QuantumExplorer and others added 2 commits September 20, 2026 23:31
…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
@QuantumExplorer
QuantumExplorer merged commit 54ed7a0 into v4.2-dev Sep 20, 2026
8 checks passed
@QuantumExplorer
QuantumExplorer deleted the claude/document-state-fee-disclosure-a4281b branch September 20, 2026 16:47
QuantumExplorer added a commit that referenced this pull request Sep 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants