feat(platform)!: moderation team reward split, action counters and reason check - #4971
Conversation
…plit and settle it before team changes A seated elected team claims its contract's moderators pot by its proposal's rewardSplit: the leader share, the equal share between the other members and the action share by each one's count of bans, suspensions, warnings and document deletions since the last settle (equally when nobody acted). Every part rounds down and the remainder stays in the pot. The counts live in the moderated contract's other tree at key 48, created with an elected contract. Every settle resets them: a claim, and the settle an addedModerator or removedModerator create or delete forces first, which ignores the once-per-epoch limit and writes no last claim. The claim proof of an elected contract's moderators pot shows the claimant's balance alone: the contract does not name a seated team. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oposal lists A moderation reason gains reasonDocumentId, the moderation charters contract's reason document the action is taken on: appended to the transition's reason, tag bit 2 where it is stored, field 4 of the gRPC reason, reasonDocumentId in wasm-dpp2. A seated elected team's ban, suspension, warning or document deletion must name a reason its proposal lists, or is refused, paid, in a block and in the mempool (ModerationReasonNotListedError, 41203). A proposal with no reason can take no such action. Reversals carry no reason and are not checked, and the interim and declared moderators are not bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sons Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eward-split-counters-reasons Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🌳 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 (2 nodes)
Changed (1 node)
Compared |
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-09-24T07:39:06.439Z |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: dashpay/platform/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (83)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThis change adds reason-document identifiers and proposal-based reason checks for seated moderation teams. It also adds elected-team moderator-pot reward splits, per-member action counts, and settlements on claims and membership changes, with corresponding storage, API, proof, and documentation updates. ChangesElected moderation team rules and settlements
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~60 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant FeeClaimValidation
participant SeatedModerationCharter
participant ModerationActionCounts
participant ModerationCharterRewardSplit
participant ModeratorsPotSettlement
FeeClaimValidation->>SeatedModerationCharter: settle_moderators_pot
SeatedModerationCharter->>ModerationActionCounts: fetch member counts
SeatedModerationCharter->>ModerationCharterRewardSplit: calculate payouts
SeatedModerationCharter->>ModeratorsPotSettlement: build payouts and count reset list
ModeratorsPotSettlement->>ModerationActionCounts: remove settled counts
Suggested reviewers: Merge Risk: ⚪ Minimal · up to Protocol version 14 adds reward-split payouts, per-member action counts, pot settlement on team changes, and a listed-reason check for elected moderation teams. The review found no concrete defect in these paths. The reported breaking storage and encoding changes are intentional and call for a devnet reset, as the author notes. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 76.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 151 functions across 44 files. (38 skipped: 7 unsupported, 6 too large, 25 over the file limit.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
|
🕓 Queued for automated review — 3rd in line, estimated start in ~1.4 h (commit 9995499)
|
- An elected contract without a counts tree (stored before the counts existed, on a development network) reads as having no counts, and its team's actions go uncounted, instead of failing inside Drive. - The claim proof shows every recipient the contract names when the claimant is one of them (interim claims keep full coverage), the claimant alone otherwise (a seated team's member). - A seated claim reads the team once; the cap on additions and the settle an addition forces share one read of the charter and its target. - Entry size estimates count the reason document id; the share reader goes through the proposal reader; the batch converter takes its settlements. - SDK docs describe the seated split and the claimant-only proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Issue being fixed or feature implemented
Item 3 of the decentralized moderation teams plan (#4865), on top of seating as reads (#4952) and deletable team changes (#4967). A seated elected team could moderate but not be paid: its claim of the moderators pot was refused, nothing counted who did the work, a team change left the pot to whoever was on the team at the next claim, and the reasons a proposal lists bound nothing. Protocol version 14, tables edited in place.
What was done?
Three commits, reviewable one by one: the pot and the counts, the reason check, the docs.
1. The seated team claims the pot, split by its proposal's
rewardSplitThe leader or an active member claims the moderators pot for the team (once per epoch, as before). The pot is paid out by the proposal's three percentages:
Rounding: every share and every part of one rounds down to the credit. What is left, a few credits, stays in the pot for the next settle, the rule the equal split of a declared team already follows, so no identity is favoured by the order of the ids. A claim that would pay nobody a credit is refused (41112, as today). The arithmetic is
ModerationCharterRewardSplit::payoutsin rs-dpp.Before, with a seated charter on the contract:
After, 10/40/50 split, pot of 1_003 credits, leader + two members + one added member, counts leader 0, member A 2, member B 1, added 0:
2. Action counts
Each counted action of a seated team member (ban, suspend, warn, delete a document) reads the signer's count and writes it one higher. Reversals (unban, unsuspend, clear warnings, restore) and the interim's actions before the seating count for nothing.
Where they live, and why: a subtree of the moderated contract's other tree at key 48,
[64, contract id, 2, 48],identity id -> Item(u32 BE), created with an elected contract (the only time elected moderation can be declared, so never lazily).maxAddedModerators) and one delete per count. A claim reads the team once (for the claimant's membership and the split), and an addition's cap and settle share one read of the charter and its target.should_keep_the_banlist_on_top_of_an_elected_contracts_other_tree_with_two_or_more_lists.Every settle deletes the counts: a claim, and the settle a team change forces.
3. A team change settles the pot first
An
addedModeratororremovedModeratorcreated or deleted (after #4967 a deletion undoes an addition or a removal, so it changes the team too) pays the pot out to the team as it was before the change, by the same split, and resets the counts. It ignores the once-per-epoch claim limit and is not a claim: it writes no last claim, so the team may still claim in the same epoch. A pot too small to pay anyone is settled all the same (the counts start over).It is an inline hook in the batch's
validate_statev0, beside the cap on additions, no new processor trait: once the change passed its own state validation (and the cap), it reads the charter, the target, the pot, the team and the counts, all billed, and the batch action carries the payouts for the batch converter to write before the change. An effect, never a refusal. At most one settle per target per batch (dormant: one transition per batch at every version).Before:
After:
4. The reason check
ContractModerationReasongainsreason_document_id: Option<Identifier>(reasonDocumentId): the moderation charters contract'sreasondocument the action is taken on. It is appended to the transition's reason, stored under a third tag bit (bit 2, the id right after the code; older values without the bit read as naming none), carried as field 4 of the gRPC reason and by wasm-dpp2.A seated team's ban, suspension, warning or document deletion must name a reason its proposal lists; otherwise it is refused, paid, in a block and in the mempool, with the new
ModerationReasonNotListedError(41203,StateErrordiscriminant 150, pinned). A reason naming none is refused the same way, so a proposal listing no reason can take no such action. The proposal is read, billed, only when a reason document is named. The code field stays as it was, unchecked.Before, seated team:
After:
Interim and declared moderators are not bound: a reason document they name is stored as written.
5. The claim's proof
The proof of a claim's execution showed the balance of every recipient, read from the contract. For a seated team the contract does not name the team (the charter contract does), so neither the prover nor a verifier holding the contract could list it. When the claimant is not among the recipients the contract names (a seated team's member), the proof now shows the pot, its last claim and the claimant's balance alone (
ContractFeePot::claim_proof_identities); every other claim, an interim team's included, proves every recipient as before. Proving the seated payees would need the charter's team documents in the proof, left for a follow-up.My calls, please confirm
In-place changes to shipped generations
validate_statev0 (drive-abci), the forced settle hookaddedModeratororremovedModerator. That contract is in state from protocol version 14 only (genesis or the upgrade to 14), and a document transition against a contract not in state fails in the transformer before this loop, as for the cap on additions already hooked there. Every other batch leaves the settlements empty.prove_state_transitionv0 andverify_state_transition_was_executed_with_proofv0 (drive), theContractFeeClaimarmDriveContractModerationMethodVersionsinDRIVE_CONTRACT_METHOD_VERSIONS_V1toV30, like the other moderation slots there: the methods are only reached from protocol version 14 code.Everything else is in generations no release selects: the moderation transition's and the claim's state v0,
insert_contract_moderation_treesv0, the converterscontract_fee_claim_transition0,contract_user_moderation_transition0 anddocuments_batch_transition1, the moderation list storage encoding, and the transition's bincode (type 24 activates at 14).How Has This Been Tested?
End to end in drive-abci (
contract_user_moderation/tests/seated_team/pot.rsandreasons.rs), throughprocess_raw_state_transitionsand check tx:should_split_a_seated_teams_claim_by_its_reward_split_rounding_each_part_down: counts 1:2:4 over a 150_000_000 action share, exact balances, 2 credits left in the pot, counts reset, last claim; a joiner the leader did not add and the interim refused.should_split_the_action_share_equally_when_the_team_did_not_actshould_count_each_signers_actions_and_reset_the_counts_at_every_settle: ban, suspend, warn, delete count; unban, unsuspend, clear, restore and the interim do not; reset by a claim and by an addition.should_settle_the_pot_to_the_team_as_it_was_before_an_addition: the added member gets nothing earned before, the settle writes no last claim and the team claims in the same epoch.should_settle_the_pot_before_a_removal_and_before_a_change_is_undone: in an epoch already claimed; undoing a removal pays the leader alone, undoing an addition pays the added member first.should_prove_a_seated_teams_claim_with_the_claimants_balanceshould_refuse_a_seated_teams_action_that_names_no_listed_reason(block and mempool, no id, unlisted, nonexistent; refused actions not counted),should_let_a_team_whose_proposal_lists_no_reason_take_no_bound_action,should_not_bind_the_interim_to_a_listed_reason.The existing seated team tests now cite a listed reason (the harness writes two
reasondocuments and the proposal lists one).Unit:
ModerationCharterRewardSplit::payouts(9 tests: rounding, remainder, equal fallback, leader alone, foreign counts, largest pot), the counts in Drive (tree created with elected contracts only, write, read, bound, reset without refunds, estimate vs applied, banlist on top), the reason encoding with bit 2, the JSON round trip ofreasonDocumentId, the discriminant pin, the converter writing the count, and a GroveDB structure fixture reaching the new node.Run locally:
cargo test -p dpp --lib moderation(98),cargo test -p drive --lib contract::moderation structure(91),cargo test -p drive-abci --lib moderation contract_fee_claim elected charter(148), earlier alsobatch data_contract_create genesis(973),cargo test -p drive-proof-verifier --lib contract_moderation(33), earlier alsocargo test -p drive --lib contract::moderation contract::fee_pots state_transition_action structure(1233), clippy-D warningson dpp, drive, drive-abci, drive-proof-verifier, dash-sdk, platform-version (all targets, all features) and on wasm-dpp, wasm-dpp2, wasm-sdk for wasm32,cargo check -p drive --no-default-features --features verify, and the wasm-dpp2ContractUserModerationTransitionspec (31 passing) against a rebuilt package. gRPC clients regenerated withyarn workspace @dashevo/dapi-grpc build.Breaking Changes
Consensus-breaking at protocol version 14 (unreleased):
reasonDocumentId(41203);[64, id, 2, 48]), so its creation costs more;A devnet already on protocol version 14 with an elected contract created before this change has no counts tree: the counts read as none there, so its seated team's actions go uncounted (the action share splits equally) instead of failing.
A claim of an elected contract's moderators pot reads the seated charter first whoever claims (any identity may be on a seated team), so a claim refused for a stranger pays that query too. The mempool prices a team change without the settle it forces, which only the block's state validation computes, as it does the cap on additions.
Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull request (described and regenerated; the viewer link is still to check)For repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
9995499/skip-botsproceeds without the ones not yet reported/self-reviewedonce the bots are donejs-wasm-sdk(packages/js-evo-sdk/src/contracts/facade.ts,packages/wasm-sdk/src/state_transitions/contract.rs) — shumkovdpp— you own itrs-drive-abci— you own itrs-drive— you own itrust-sdk(packages/rs-sdk/src/platform/transition/contract_fee_claim.rs) — lklimek or shumkovWhen every box is checked the
PR Hygienecheck passes and this can merge.