Skip to content

Decentralized moderation teams: masternode-elected moderators per contract (decisions and open questions) #4865

Description

@QuantumExplorer

Summary

A data contract can declare that its moderation team is chosen by masternodes and evonodes instead of by the contract owner. Teams apply by publishing a charter, masternodes elect one, and the seated team moderates the contract's content under the limits of its charter, paid from the contract's moderators fee pot. A seated team can only be replaced by a challenge vote.

Design rounds 1 to 10 ran 2026-09-19 to 2026-09-22 (the round comments below hold the history); on 2026-09-23 the charter was reshaped while it was built (PR #4898), see Changes on 2026-09-23 below. This body is the consolidated design. Built so far: the contract-side declaration, the no-lock contest resolution, warnings, the fee agreement recheck and the schema features the charter contract needs; the charter contract itself is in review. Target: protocol version 14 on v4.2-dev, version tables edited in place.

Changes on 2026-09-23

Decided by the owner while PR #4898 was built; the decisions below are marked where they changed.

  • The single charter document became seven types in the charter contract: reason, submittedCharter, joinRequest, electedCharter, addedModerator, removedModerator, resignationRequest (D66).
  • No powers anywhere: any one member acts alone (D67; supersedes the power parts of D1, D2, D9, D28 to D31, D35, D44).
  • No claimed abilities: a seated team holds every ability the contract's declaration gives it; it narrows what it acts on only through the reasons it lists (D68; amends D25, D62, D64).
  • Reasons are documents referenced by id, not numeric codes (D69; supersedes D50).
  • Members come from join requests, identities that asked to join a proposal (D70).
  • After the election the leader adds members from join requests, up to maxAddedModerators on the contract's declaration, and removes members; a member may file a resignation request (D72; supersedes D4).
  • Threshold (co-signed) moderation actions, challenges and amendments leave protocol version 14 for 4.3 or 4.4; a 4.2 contract still declares the challenge parameters and they are validated (D73).
  • Seating writes nothing: a contract's team is read from its stored elected charter (D74; supersedes the storage part of D15).
  • A resignation request is a request the leader acts on with a removal (D75).
  • Once a contract has a seated charter, interim moderators can no longer act (D76).
  • A discounted moderators fee is checked against the seated charter only when an action pays less than the declared amount (D77).
  • The contract's declaration says whether its seat can be contested again once a team is seated (seatContestable, required); the contested key stays the target contract alone, and a challenge will be a new contest on the same index (D78; amends D8's cool-down rule, resolves the contested key question).

It builds on what already merged:

PR What it gives us
#4830 Contract moderation: ContractModerators::{ContractOwner, AppointedModerators}, banlist + suspensions, ContractUserModeration state transition (type 24), actor-level gate in the batch transformer
#4849 A reason on every ban and suspension: ContractModerationReason { code: Option<u16>, text <= 1024 bytes }, the code is never checked today
#4857 Moderators delete documents of types flagged canBeDeletedByModerators, permanent removal record, storage refund forfeited
#4864 canBeDeletedByModeratorsFor: a window after a document's last modification for moderator deletes
#4851, #4856 Document action fees (fixed credits per action, immutable) into an owner pot and a moderators pot, ContractFeeClaim (type 25), equal split between team(), once per pot per epoch, last claim record, getContractFeePots
#4858 $actionFeeAgreement: a document transition states the action fee it agrees to pay, with an increase tolerance
#4886 The elected ContractModerators declaration: frozen windows and cool-down, moderated set with abilities, interim, owner flag (stack item 1, contract side)
#4907 ContestedIndexResolution::MasternodeVoteNoLocking ("resolution": 1) and earliest-contender ties at v14 (D21 to D23, D56)
#4872, #4884 Warnings (D49), and a moderation reason that names the documents it is about
#4900, #4904 RecheckTx re-judges $actionFeeAgreement; a vote on an unfunded poll is refused unpaid
#4909, #4914 Contract references may require moderation: "elected" or "electionOpen"; the contract declares its own electionDelay before the first election
#4922 to #4924, #4928, #4930 Typed arrays, refersTo on their elements, and lookup references resolved through a unique index
#4917, #4918, #4916, #4919 distinctFrom, keyRequirements on key references, key references on the writer's own identity, the encryptedFor envelope

Decisions

1. Contract-side declaration (frozen at creation)

  • D3, D7. A third ContractModerators variant, exclusive with the two merged ones. Declarable only at contract creation; once elected, never out. (feat(platform)!: contract moderation with a banlist and a suspension list #4830 already refuses enabling moderation on update.)
  • D8 (cool-down amended 2026-09-24 by D78). Election parameters are frozen once set. Seconds as u32, bounds in SystemLimits: join window and vote window 1 day to 4 weeks, default 1 week each; challenge cool-down x 2 weeks to 3 years, declared only for a contestable seat (required when seatContestable is true, refused when it is false).
  • D25 (amended 2026-09-23). The contract declares which document types it moderates (the moderated set). The charter never chooses types, and since D68 it does not choose abilities either: it says on what grounds the team acts (its reasons), what it charges and how it splits the pay.
  • D47. The moderated set may include types not flagged canBeDeletedByModerators. Deletes reach only the flagged types, within each type's window. Bans and suspensions are not limited by the flag.
  • D62, D64; D63 dropped (2026-09-22, round 9; amended 2026-09-23 by D68). The contract declares, per moderated document type, the abilities the seated team holds on it (non-empty); a charter claims none of them. There are no charter fee maximums: a type's own actionFees.moderators amount is the most a team may charge on its actions, and a charter charges a share of it, so the signer's fee agreement to the declared amounts never mismatches a seated charter. Owner pot amounts stay contract-fixed and immutable.
  • D6, D17, D18. Interim mode until the first team is seated: the owner moderates, an appointed set moderates (the merged kinds, no power group), or the moderated types are not yet usable. "Not yet usable" blocks only the moderated types; if no team ever comes those types are dead forever. Accepted. A fourth interim, noModeration (2026-09-22, round 9): nobody moderates and nobody claims the pot until a team is seated, but the moderated types stay usable meanwhile.
  • D19, D26. Interim bans, suspensions and removals survive the first seating. Interim moderators may claim the pot until seating; at the first seating the pot is not settled, it carries over to the elected team.
  • D20, D27. The contract says whether the owner is protected from moderation in elected mode, default not protected. The leader and the current members are always protected (41102).
  • D78 (2026-09-24). The declaration says whether the seat can be contested again once a team is seated: seatContestable, a boolean, required with no default, frozen with the rest. A default of false would make every team permanent, removing the only remedy D11 names for a lost-keys or rogue leader; a default of true would opt every contract into challenges silently. challengeCoolDown is required when it is true and refused when it is false (in Rust the two are one challenge_cool_down: Option<u32>). The charter contract's electedCharter.byTargetContract index stays keyed by the target contract alone: a challenge (4.3 or later, D73) is a new contest on that same unique index, allowed only when the target declares its seat contestable; no round component, no separate challenge document type. No behaviour change in 4.2: an electedCharter create for a seated target stays refused whatever the key says (40105). PR feat(platform)!: elected moderation declares whether its seat is contestable (PV14) #4969.

2. Charter and team

  • D15, D57, D59, D60 (amended 2026-09-23 by D66). Charters are documents of a new system data contract. An application is an electedCharter document create on a contested unique index keyed by the target contract id, reusing the DPNS contest machinery with the windows and the fee read from the target contract. The seated team (leader, members, active charter id) lives under the moderated contract for a cheap read. (Superseded 2026-09-24 by D74: nothing is stored at seating.) Charter documents are immutable; an amendment is a new charter document plus a vote that swaps the active charter id.
  • D1 (amended twice; 2026-09-23), D58. Consensus enforces the reasons, the moderators share and the split from the charter, and the document types and abilities from the contract. The "what we moderate" text is informational, capped at 4096 bytes.
  • D50 (superseded 2026-09-23 by D69). The charter lists its reason codes (1 = hate speech, 2 = doxing, ...). Every moderation action's code must be one the seated charter lists.
  • D2, D9, D28, D29, D30, D31 (powers superseded 2026-09-23 by D67, members by D70). The applicant pays, is the contender, and becomes the leader. The initial members are part of the application and are seated as-is, at most 15 besides the leader (16 in all). A team is a group with power. The leader assigns powers freely. The charter sets a required power per ability. A membership change that leaves any ability unreachable is refused.
  • D10, D11, D33. One team per contract. The leader cannot change. A challenge is the only remedy for a lost-keys or rogue leader, and only on a contract that declares its seat contestable (D78).
  • D4 (resignation superseded 2026-09-23 by D72). No terms. No resignation.

3. Abilities

  • D48. Silence is the merged suspension, as-is.
  • D49. Warn is an on-chain record with no consensus effect and no expiry. In scope.
  • D51. Ban and suspension scope stays contract-wide. The moderated set limits deletes and the interim block only.

4. Fees and the split

  • D45, D65 (amended 2026-09-22 by D63 dropped, and 2026-09-23). The charter sets moderatorsShare, one optional percentage of each moderated type's declared actionFees.moderators amount: absent is 100, a lower number a discount, 0 a team that takes no rewards. $actionFeeAgreement protects users. New amounts apply immediately at the block a seating or amendment passes; the agreement's tolerance absorbs small rises, larger ones fail at recheck and the user re-signs. Needs validate_action_fees_unchanged lifted for the moderators part and the RecheckTx gap closed. Teams compete on price.
  • D34, D38. The claim split is written in the charter, three percentages summing to 100, free within 0 to 100: a leader share, a share split equally between the other members, and a share split by each member's moderation action count. Example 10 / 40 / 50.
  • D35, D36, D37 (thresholds gone 2026-09-23 by D67). Only actions the contract allows count, reversals excluded, farming accepted (the leader removes a farmer). An action has one signer, who gets the count. Counters reset at every settle; with no actions in the period the action share is split equally.
  • D32, D13. Adding and removing a member (D72) both force a settle of the moderators pot first, ignoring the once-per-epoch limit. At a seat change between elected teams the pot is settled to the outgoing team first.

5. Initial election

  • D21, D22, D23, D43, D52, D53 (round 10, 2026-09-22). The election is the contested unique index of D15 resolved a second way: ContestedIndexResolution::MasternodeVoteNoLocking ("resolution": 1, PR feat(platform)!: contested indexes resolved without locking, ties to the earliest contender (moderation teams A1) #4907), a general resolution any contract may use, not a separate poll kind (feat(platform)!: identity contender vote polls without Lock, plurality, first-wins tie-break (moderation teams A1) #4901 closed). No Lock choice. The applicant with the most votes wins, plurality, no minimum; with zero votes the first applicant wins. "First" is document creation time, then block height, then core height, then document id. A single applicant wins when the join window closes, with no vote window: the contest's end date is the join window's end until a second applicant joins, when it moves to the full poll duration.
  • D24, D54, D55, D16. Applying (an electedCharter create) costs 0.5 Dash; filing a proposal, a reason or a join request is an ordinary document fee. Applying prefunding the masternode votes, the remainder released as processing fees at clean-up. The amounts are new fields of VoteResolutionFundFees in rs-platform-version, shared by application, challenge and amendment polls. No extra spam limit.
  • D56 (revised round 10). From protocol version 14 every contested index, DPNS included, awards a tie to the EARLIEST contender; the shipped latest-wins rule was a mistake. Contests ending before v14 keep it (check_for_ended_vote_polls v0 stays frozen, v1 uses min_by).

6. Challenges and amendments

  • D40, D41, D42. A challenge runs in two stages. The first challenger opens it and pays; the contract's join window admits more challengers, each paying the same fee; nobody joins after it closes. Stage 1, "replace the incumbent?", runs for the vote window and passes when yes power exceeds two thirds of yes plus no (abstain excluded) and yes plus no is at least 400 (evonode 4, masternode 1). Stage 2, "which challenger?", is a contender poll with plurality and first-wins, skipped with a single challenger. The winner is seated immediately. The challengers' fees pool into one prefunded balance for both stages; nothing is refunded on failure.
  • D39, D46, D61. Challenges are allowed only against a contestable seat (D78), at most once every x, counted from the last seat change. A failed challenge starts no cool-down; instead the fee doubles for each challenge opened within 3 months of the previous challenge's close (relative to the previous fee, reset to base after 3 months, system constant, saturating). Amendments have their own clock of the same length, reset by a passed amendment, so an amendment never shields a team from a challenge.
  • D44 (powers gone 2026-09-23). The leader prefunds an amendment vote. An amendment may run while a challenge is open.
  • D12. Bans and suspensions persist across a handover.

7. Reshaped charter (2026-09-23)

  • D66. The charter contract has seven document types, all immutable and undeletable except resignationRequest: reason; submittedCharter (a leader's proposal: target, description, reasons, optional moderatorsShare, rewardSplit); joinRequest (an identity's offer to serve on a proposal); electedCharter (a proposal put to the vote with its members, the only type on the contested index); addedModerator, removedModerator and resignationRequest (changes after the election).
  • D67. No powers. Any one member of the seated team acts alone.
  • D68. A seated team holds every ability the contract's declaration gives it on each moderated type. A team narrows what it acts on only through its reasons.
  • D69. A reason is a document: three uppercase letters unique per owner, a label, an optional description; anyone may file one and teams may share them. A proposal lists at most 64 reasons by document id. Every moderation action names one reason document the seated proposal lists; a proposal with none can take no action.
  • D70. A joinRequest is an identity's consent to serve on a proposal, one per identity per proposal, with a message only the leader can read (ECDH on the leader's decryption key bound to submittedCharter and the member's encryption key bound to joinRequest). An elected charter's members must each have filed one for its proposal (a lookup through the join request's unique index), and the leader is never a member.
  • D71. A proposal may be filed as soon as the target declares elected moderation, so teams form during the contract's own electionDelay; an elected charter needs the delay to have passed. Only the proposal's owner may put it to the vote, for the proposal's own target. An identity may contend once per contest, so a proposal has at most one contender at a time.
  • D72. After the election the leader may add members from the proposal's join requests (addedModerator), at most maxAddedModerators on the contract's declaration (0 when left out, at most 15, counting additions ever filed, so a removal frees no slot), and remove members (removedModerator, final, no resignation needed). A member may file a resignationRequest, with a message to the leader, and may delete it; only a member of the seated team may file one (D75). The team that acts is the leader plus the elected members and the additions, less the removals.
  • D74 (2026-09-24). Seating writes nothing. The seated charter of a contract is its electedCharter in the charter contract's storage, found through the byTargetContract index: only a contest's winner is ever written there (contenders live in the contest), and in 4.2 a seat is never replaced (D73). Its team is the leader (the charter's owner) plus its members and the addedModerator members, less the removedModerator members. The moderation paths read this; there is no block-end seating hook and no copy under the moderated contract. Supersedes the storage part of D15.
  • D75 (2026-09-24). A resignationRequest changes nothing by itself: the leader acts on it with a removedModerator, and the member withdraws it by deleting it. (A resignation that took effect when filed could be deleted to rejoin without the leader or an addition slot.)
  • D76 (2026-09-24). Once a contract has a seated charter, a moderation action by an interim moderator is refused; only the leader and the active members of the seated charter moderate.
  • D77 (2026-09-24). A document action on a moderated type whose $actionFeeAgreement names the full declared moderators amount is accepted without reading the seated charter, as today, and is charged that amount. One that names less is checked against the seated charter: the charter's moderatorsShare applied to the declared amount must give the agreed amount; with no seated charter, an agreement below the declared amount is refused. So only discounted actions pay for the charter read.

8. Scope

Open questions

  • The contested key is the target contract alone (D57). Amendments (D60) and challenges create further charters for the same target, so the key needs a seat or round component before stack item 4, now 4.3 or 4.4 (D73). Either the v1 schema takes it while 4.2 is unreleased, or challenges and amendments become their own document type with their own contested index in 4.3; changing electedCharter's index after 4.2 ships means re-indexing stored documents. Resolved 2026-09-24 by D78: the key stays the target contract alone, and a challenge is a new contest on the same unique index, allowed only when the target declares seatContestable. How an amendment (D60) fits the single key is left to its design with challenges in 4.3 or 4.4.
  • Rounding of a discounted fee (D77): how the share is applied to the declared amount (rounded down, or exact multiples only).

Findings in the code that shape the work

  • The shipped contested-document tie-break awarded the LATEST contender (check_for_ended_vote_polls v0, max_by(created_at, ...)); v1 at protocol version 14 awards the earliest for every resolution (D56).
  • The contested document fund is 0.1 Dash at PV14 (VOTE_RESOLUTION_FUND_FEES_VERSION2), the single vote cost 0.0001 Dash, and a vote is refused once the prefunded balance is empty. 0.5 Dash (D54) covers 5,000 votes.
  • clean_up_after_contested_resources_vote_polls_end v1 already releases the remainder of a prefunded balance as processing credits.
  • VotePoll has a single variant; feat(platform)!: yes/no masternode vote poll kind with supermajority and minimum voting power #4899 adds the yes/no poll for challenges. YesNoAbstainVoteChoice already exists in rs-dpp.
  • The merged claim pays equal shares to team() once per pot per epoch and leaves the remainder. D32 and D13 are forced claims that run before the membership or seat change; D34 replaces the equal split.
  • The merged moderation gate is actor-level and contract-wide, which D51 keeps. Per-type scope is needed only for deletes (already per type) and the interim block.
  • The contract's "other" tree [64, contract_id, 2] already holds keys 16, 32, 64, 96, 128, 192, and tests pin which key tops it. The seated team, the action counters and the two cool-down clocks need a home there or their own.
  • ContractModerationReason.code is stored unchecked today; D69 replaces it with a reason document id checked against the seated proposal's list.
  • A contender's document lives in its contest, not in the document type's storage, until it wins. So a type with a contested unique index may carry no other unique index (fix(platform)!: document types should not have a contested unique index with a unique index  #1984), and a reference to an electedCharter can only find a seated one.
  • A contested type must be immutable, so team changes after the election are separate documents (D72), never edits of the elected charter.
  • The schema cannot count documents: the maxAddedModerators cap is a consensus rule in stack item 3.

PR stack

Status on 2026-09-24.

  1. Precursor, vote polls: the no-lock resolution and earliest ties (feat(platform)!: contested indexes resolved without locking, ties to the earliest contender (moderation teams A1) #4907, merged), unfunded-poll refusal (fix(drive-abci): refuse a masternode vote on an unfunded poll as an unpaid consensus error #4904, merged), the yes/no poll for challenges (feat(platform)!: yes/no masternode vote poll kind with supermajority and minimum voting power #4899, draft, deferred with challenges by D73). Not built: the 0.5 Dash fund fields (D54) and the windows and fee read from the target contract.
  2. Charter contract and declaration. Done. Declaration feat(platform)!: elected moderation team declaration with frozen election parameters and interim mode #4886 with electionDelay (feat(platform)!: elected contracts declare their own election delay, read by the electionOpen reference requirement #4914), maxAddedModerators and seatContestable (feat(platform)!: elected moderation declares whether its seat is contestable (PV14) #4969, open); the charter contract feat(platform)!: moderation charters system data contract #4898 (seven types, written to state at genesis from 14 and on the upgrade to 14). Schema features it uses: feat(platform)!: contract references may require the referenced contract to declare elected moderation #4909, feat(platform)!: elected contracts declare their own election delay, read by the electionOpen reference requirement #4914, feat(platform)!: key references on the writer's own identity (PV14) #4916 to feat(platform)!: encryptedFor envelope declaration on byte properties (PV14) #4919, feat(dpp)!: typed scalar arrays in document schemas (PV14) #4922 to fix(dpp)!: typed array review fixes: hyphenated list paths, element constraints, untrusted lists, Swift refusal #4924, feat(platform)!: refersTo on typed array elements (PV14) #4928, feat(platform)!: document references resolved through a unique index (PV14) #4930, feat(platform)!: listElement references into a list of a referenced document (PV14) #4940 to feat(platform)!: anyOf and allOf reference expressions (PV14) #4942.
  3. Seating as reads (D74 to D77). The moderation gate and moderation actions read the seated charter: the team moderates and is protected, interim moderators stop (D76), the maxAddedModerators cap on additions, discounted fees checked against moderatorsShare (D77). Not started. Election parameters read from the target (windows, the D54 fund) are still part of item 0.
  4. Team changes, reason check, split. The maxAddedModerators cap, forced settles before a change, the three-part split with action counters, the reason check, resignations. Not started.
  5. Challenges and amendments, deferred to 4.3 or 4.4 (D73) (two stages, escalating fee, cool-down clock; own clock, active charter swap). A challenge is a new contest on byTargetContract, only against a contestable seat (D78). Not started.
  6. Warn. Done: feat(platform)!: a warning list for moderated contracts #4872, feat(platform)!: a moderation reason names the documents it is about #4884.
  7. DAPI queries, proof verifier, rs-sdk, wasm/JS. Mobile later. Appeals separate.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions