feat(platform)!: document action fees paid to the contract owner and moderators, with a fee claim state transition - #4851
Conversation
The `actionFees` keyword (v3 document meta-schema, protocol version 14) sets a fixed fee in credits per document action, split between the contract's owner pot and its moderators pot, priced as written or scaled by the epoch fee multiplier. The fees are fixed when the document type is published. Adds the moderation team helper and the consensus errors of the fee claim. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two sum trees under the prefunded specialized balances hold what every contract's document action fees have collected: the owner pot and the moderators pot. They sit under a root sum tree so the credits stay inside the sum every block is checked against. The epoch each pot was last claimed in is an item of the contract's other tree. Versioned add, deduct, fetch, marker and prove methods, the verifier, cost estimation, the drive operation and the structure description; created at genesis (structure v4). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The base document action carries the fee its document type declares for the action and how it is priced. The batch action carries the fees once priced, and action_fee_operations turns them into operations for whoever pays: one removal from the payer, one addition per fee pot, the owner part dropped when the payer is the contract owner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A document transition on a document type that declares an action fee moves the fee from the gas payer's balance into the contract's owner and moderators pots, with the batch's own operations. The signer pays, or the contract owner when they sponsor the gas; fee validation and execution settle the payer with one formula on one estimate. The owner never pays into their own owner pot, a transition that became a nonce bump owes nothing, and the fee is no part of the fee result, so the fee pools see what they saw. A fee priced by the fee multiplier follows the multiplier of the epoch, read once per batch and billed. A moderators fee needs declared moderation (10902). The pot trees are created by the upgrade to protocol version 14 as at genesis. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ContractFeeClaim (type 25, protocol version 14) pays out one of the two fee pots a contract's document action fees collect in. The owner pot goes to the contract owner, who alone may claim it; the moderators pot is split equally between the moderation team, any member of which may claim it, and what the split leaves over stays in the pot. Each pot is paid out at most once per epoch, on its own clock. A refused claim is paid for by a nonce bump and leaves the pot alone. The execution proof shows the pot, its last claim epoch and the balance of everyone it paid. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sm-dpp Boxing keeps the batched transition enum under clippy's large-variant threshold: most actions declare no fee. The legacy wasm-dpp matches gain the new consensus errors and refuse the new transition like the other wasm-dpp2-only ones. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…laim Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ClaimContractFees sends a ContractFeeClaim and resolves with the proved pot and balances. The proof is verified against the contract, which names who the pot pays, so the contract is registered with the context provider before anything is signed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…in the protocol version 14 changelog Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ContractFeeClaim class, its arms in the state transition wrapper and the VerifiedContractFeeClaim proof result. The credits left in the pot are a number in JSON while that is exact in JavaScript and a decimal string past it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important Review skippedToo many files! This PR contains 186 files, which is 86 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (186)
You can disable this status message by setting the 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 |
|
🌳 GroveDB structure This pull request changes the described GroveDB structure. Open it in the structure viewer: new nodes glow, removed ones stay as ghosts, and the tour walks through each change. Added (6 nodes)
Changed (2 nodes)
Compared |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-20T11:06:23.499Z |
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 — 3rd in line, estimated start in ~55 min (commit 6dbd6cb)
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v4.2-dev #4851 +/- ##
============================================
- Coverage 84.70% 84.18% -0.53%
============================================
Files 3063 3100 +37
Lines 411520 417212 +5692
============================================
+ Hits 348587 351226 +2639
- Misses 62933 65986 +3053
🚀 New features to boost your workflow:
|
- The first fee a pot receives creates the pot's tree when it is missing, so a chain that reached protocol version 14 before the pots existed can use them. - Fee arithmetic is held at the maximum credits instead of overflowing: a fee nobody can pay is an insufficient balance, not an internal error. - A stored contract whose action fees charge the moderators without declared moderation cannot have been validated and is read as declaring none, so no credits collect in a pot that has no team. - The voting balance estimation gets a protocol version 14 generation that describes the prefunded balances layer as the three trees it now holds. - The SDK fetches the contract again before a claim: the team can change, and a stale copy would refuse a claim that executed. The registration is shared with the moderation transition. - What leaves the payer is summed from what reaches the pots. - Tests: every action charges its own fee, check tx with the fee multiplier pricing, the first block of an epoch through a block, a live contract gaining fees through a new document type, and 10902 on a contract update. The claim tests share the moderation tests' actor and assertions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate-transition-fees-moderator-d1cd46 # Conflicts: # book/src/data-model/contract-moderation.md # book/src/error-handling/error-codes.md # packages/rs-dpp/src/errors/consensus/basic/basic_error.rs # packages/rs-dpp/src/errors/consensus/codes.rs # packages/rs-drive/grovedb-structure.json # packages/rs-drive/src/structure/tests.rs # packages/wasm-dpp/src/errors/consensus/consensus_error.rs # packages/wasm-dpp2/src/state_transitions/proof_result/data_contract.rs
Every doctype-level keyword of generation 3 is read wherever it appears and its shape is enforced on the validating and the stored path alike. actionFees now follows that rule like indexOnly, immutable and the aggregate keywords: a malformed declaration is an error on both paths, and the arm that read a moderators fee without moderation as no declaration is gone, since creation refuses that state (10902) and moderation is never turned off. The leniency guarded against a stray key in a contract admitted under the open v0 meta-schema. A census of every contract create and update on mainnet and testnet (2026-09-20) found none, and every create and update since protocol version 12 is validated by a meta-schema that refuses unknown doctype-level keys, so every declaration a node reads from state was validated. The leniency was also the wrong tool: it could only soften the malformed case, never the well-formed one, and full_validation false is also what check tx and client parsing pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sum of a claim's payouts and an addition to a fee pot reported a failed checked_add as corrupted code execution and as a critically corrupted state. Both are overflows, and Drive's fee arithmetic has an error for that. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Reviewed |
Issue being fixed or feature implemented
Follow-up to #4830 (contract moderation). A contract can now pay its owner and its moderation team: a document type may charge a fixed fee in credits for an action on one of its documents, on top of the gas, and the collected fees are paid out by a new state transition. Protocol version 14, tables amended in place like #4830.
Creating a post costs an extra 0.001 Dash for the moderation team and 0.0001 Dash for the contract owner.
What was done?
Declaration (
rs-dpp). TheactionFeeskeyword in the v3 document meta-schema, besidetokenCost, prices the same six actions (create,replace,delete,transfer,update_price,purchase), each with anownerand amoderatorspart.pricingisfeeMultiplier(the default: the declared amounts are scaled by the fee multiplier of the epoch) orfixed. Parsed bytry_from_schemageneration 3 intoDocumentTypeV2.action_fees.validate_updatev1 refuses adding, changing or removing theactionFeesof an existing document type. A document type added by an update may declare its own, which is how a live contract gets fees.moderatorspart needs a contract that declares moderation (DocumentActionFeesWithoutModerationError, 10902), checked in contract create and updatebasic_structurev2.ContractModerators::team(): the identities that share the moderators pot are the ones the contract appoints, the owner among them only when appointed, and the owner alone when nobody is appointed.The pots (
rs-drive).[PreFundedSpecializedBalances, 64, contract id]is the owner pot and[PreFundedSpecializedBalances, 192, contract id]the moderators pot, both sum items. They are deliberately not under the contract:calculate_total_credits_balancesums a fixed set of root sum trees,DataContractDocumentsis a normal tree, and credits parked there would fail every block withCorruptedCreditsNotBalanced. The two sum trees are created at genesis (state structure 4) and bytransition_to_version_14through the same helper. The epoch each pot was last claimed in is a plain item of the contract's other tree (keys32and96). All of it is described instructure.rsandgrovedb-structure.json; the recorded Merk shapes show the voting balances still on top of the prefunded layer and the banlist still on top of the contract's other tree.estimated_cost_for_prefunded_specialized_balance_update: 1) that describes it that way; the shipped v0 is untouched.Charging (
rs-drive-abci). Whoever pays the gas pays the action fee: the signer, or the contract owner when they sponsor the gas (#4826). That is only settled in fee validation, so the fee travels onExecutionEvent::Paid.action_feesand one function,action_fee_operations(payer, fees), builds the removal from the payer and the additions to the pots where the payer is chosen.validate_fees_of_eventv1 andexecute_eventv1 (both protocol version 14 only) ask one question through one function,gas_sponsor_pays, on one estimate, so they always name the same payer.FeeResult: the fee pools and the proposers see what they saw before. What leaves the payer is summed from what reaches the pots, so the two cannot drift.ContractFeeClaim(state transition type 25). Names a contract and a pot (ownerormoderators) and pays the pot out: the owner pot whole to the contract owner, who alone may claim it; the moderators pot in equal shares to the team, any member of which may claim it. What the split leaves over stays in the pot, so no member is favoured by identity id order. Each pot is paid out at most once per epoch and the two are independent. A refused claim is paid for by a nonce bump and leaves the pot alone. Errors 41111 (already claimed this epoch), 41112 (nothing to claim), 41113 (not a recipient of the pot). The execution proof shows the pot, its last claim epoch and the balance of every recipient (VerifiedContractFeeClaim).Clients.
ClaimContractFeesin the Rust SDK, theContractFeeClaimclass and proof result inwasm-dpp2, the new errors in the legacywasm-dpp. The proof of a claim is verified against the contract, which names who the pot pays, and the team can change by a contract update, so the SDK fetches the contract again before a claim rather than trust a copy the context provider holds; that registration is one helper shared with the moderation transition. Book:fees/overview.md,data-model/contract-moderation.md,error-handling/error-codes.md.Three things the tests caught that are worth a reviewer's eye
resolved_action_fees()reads the fees off the final transitions when the execution event is built.fetch_action_fee_multiplier_with_feefalls back to the fee schedule's multiplier, which is what the epoch is about to be initialized with. Without it every fee-bearing transition of the first block of each epoch would have been an internal error.actionFeeskey in an old contract, and why it needs no special case. The frozen v0 document meta-schema (protocol versions 1 to 11) does not forbid unknown top-level keys, and stored contracts are parsed by generation 3 from protocol version 14, so a contract from that era could in principle carry a key of this name that nobody validated. An earlier revision of this PR read such a declaration as none on the stored path. That is gone: like every doctype-level keyword of generation 3,actionFeesis read wherever it appears and its shape is enforced on the validating and the stored path alike, which is whatindexOnly,immutableand the aggregate keywords do and what feat(sdk): show and lock immutable document properties in the mobile example apps #4820 pins. A census of every contract create and update on mainnet and testnet (2026-09-20), decoded from the raw bytes, found no contract admitted under meta-schema v0 carrying the key (mainnet: 54 contract versions before protocol version 12, zero hits; testnet: 3347, zero hits), and that set is closed, since every create and update since is validated by a meta-schema that refuses unknown doctype-level keys. So every declaration a node reads from state was validated. The gate test still pins the precondition: protocol versions 1 to 11 admit and ignore the key, 12 and 13 refuse it, 14 reads it.Not in this pull request
getContractFeePots) with itsrs-sdkFetch,wasm-sdkandjs-evo-sdksurfaces, and awasm-sdk/js-evo-sdkmethod for the claim. The Drive side is ready for it (fetch_contract_fee_pot,prove_contract_fee_pots,verify_contract_fee_pots).actionFeesjoins its list of malformed keywords at the merge.How Has This Been Tested?
Targeted suites, all green locally:
rs-dpp: keyword parsing, both pricings, malformed declarations refused on the validating and the stored path, a moderators fee parsing on both paths (the contract is what refuses it), the protocol version gate (ignored at 1 to 11, refused at 12 and 13, read from 14), the update rule,team(), amounts held at the maximum credits, the frozen error discriminants, the transition's bytes, JSON and value round trips.rs-drive: pot add, deduct and fetch, a chain whose pot trees are missing, the last claim epochs, proof round trips, the pots counted bycalculate_total_credits_balance, estimation against applied cost, the multiplier read including the first block of an epoch,action_fee_operations, and the structure conformance tests against a real GroveDB.rs-drive-abci:feeMultiplieragainstfixedwith an epoch multiplier of 1500, through check tx as well; the first block of an epoch through a block; the owner's own document; a paying sponsor with an unfunded user; an insisted sponsor that covers the gas but not the fee (40222, unpaid); a preferred sponsor that falls back to the signer; a signer who cannot afford the fee; a transition that fails pays no fee; a budgeted key;v4.2-dev: 2,245rs-dpptests (document_type,state_transition,consensus), 1,333rs-drivetests (prefunded,votes,fee_pots,state_transition_action,structure) and 620rs-drive-abcitests (document batches, contract create and update, moderation, the claim, fee validation and execution, masternode votes and voting, the protocol upgrade), all passing. After that merge the suites around everything it touched were run again and pass. The full suites and the strategy tests are left to CI.cargo clippy --all-targets -- -D warningsondpp,drive,drive-abci,platform-version(with--all-features),dash-sdk,wasm-dpp,wasm-dpp2andwasm-sdk;cargo check -p drive --no-default-features --features verify;cargo check --workspace --all-targets.wasm-dpp2TypeScript spec (ContractFeeClaimTransition.spec.ts), which needs a wasm build. It mirrors the type 24 spec.Breaking Changes
Consensus-breaking, gated at protocol version 14: a new document type keyword, a new state transition type (25), new GroveDB structure under
PreFundedSpecializedBalancesand the contract's other tree, new consensus errors (10902, 41111 to 41113), a new generation of the voting balance cost estimation, and document batches on a fee-bearing document type move credits into the pots. Nothing changes for a contract that declares noactionFees.One point is consensus-relevant only in theory: a stored contract with a malformed
actionFeesrefuses to load, as it does for every other generation 3 keyword, instead of reading as a contract without fees. The census above shows no such contract exists on mainnet or testnet, and none can be created.Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull request🤖 Generated with Claude Code