Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions book/src/data-model/contract-moderation.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ A moderation team can be paid. A document type may charge a fixed fee in credits
└── [192] moderators fee pots (sum tree) -> <contract id> -> SumItem(credits)

[64] DataContractDocuments -> <contract id> -> [2] other
├── [32] epoch the owner pot was last claimed in Item(u16 BE) (after a claim)
└── [96] epoch the moderators pot was last claimed in Item(u16 BE) (after a claim)
├── [32] last claim of the owner pot Item(epoch u16 BE | time u64 BE | claimant id) (after a claim)
└── [96] last claim of the moderators pot Item(epoch u16 BE | time u64 BE | claimant id) (after a claim)
```

The pots are not under the contract. The per-block total credits check (`calculate_total_credits_balance`) sums a fixed set of root sum trees, and `DataContractDocuments` is a normal tree: credits parked under a contract would leave that sum and fail every block with `CorruptedCreditsNotBalanced`. `PreFundedSpecializedBalances` is one of the summed trees, so the pots live there, in two sum trees beside the voting balances, created at genesis (state structure 4) and by the upgrade to protocol version 14 through the same helper, one after the other, so that both node populations build the same Merk. A pot is created by the first fee it receives, and so is its tree on a chain that reached protocol version 14 on a build from before the pots: that first fee checks, with a billed read, that the tree is there. The estimation of a voting balance write moves to generation 1 with them, because the prefunded balances layer now holds three trees instead of one. The two last claim epochs are plain items of the contract's other tree, below `128` so the banlist stays on top, written by the first claim.
The pots are not under the contract. The per-block total credits check (`calculate_total_credits_balance`) sums a fixed set of root sum trees, and `DataContractDocuments` is a normal tree: credits parked under a contract would leave that sum and fail every block with `CorruptedCreditsNotBalanced`. `PreFundedSpecializedBalances` is one of the summed trees, so the pots live there, in two sum trees beside the voting balances, created at genesis (state structure 4) and by the upgrade to protocol version 14 through the same helper, one after the other, so that both node populations build the same Merk. A pot is created by the first fee it receives, and so is its tree on a chain that reached protocol version 14 on a build from before the pots: that first fee checks, with a billed read, that the tree is there. The estimation of a voting balance write moves to generation 1 with them, because the prefunded balances layer now holds three trees instead of one. The two last claims are plain items of the contract's other tree, below `128` so the banlist stays on top, written by the first claim and replaced by every later one. A last claim (`ContractFeePotLastClaim`) is 42 bytes: the epoch of the claim, which the next claim is judged against, the time of its block in milliseconds, and the id of the identity that signed it. The owner pot's claimant is always the owner; the moderators pot's is whichever member of the team claimed for all of them, so the team can see who paid them and when. Every last claim has the same size, so a replacement never changes the size of the item, and the item carries no storage flags: it is never removed, and no claim adds bytes for anyone to own.

**The team** that shares the moderators pot is the set of identities the contract appoints, the owner among them only when appointed, and the owner alone when nobody is appointed (`ContractModerators::team`). It is about earnings, not authority: an owner who is not appointed still may moderate. `ContractFeePot::recipients` names who a payout of a pot goes to: the contract owner for the owner pot, the team for the moderators pot, nobody for the moderators pot of a contract that declares no moderation.

Expand All @@ -159,19 +159,29 @@ The pots are not under the contract. The per-block total credits check (`calcula
| | the pot was not paid out in this epoch yet | 41111 |
| | every recipient gets at least a credit | 41112 |

The owner pot goes to the owner whole. The moderators pot is split equally between the team, and what the split leaves over, less than a credit per member, stays in the pot for the next claim, so no member is favoured by the order of the identity ids. Each pot is paid out at most once per epoch and the two are independent: the owner's claim does not use up the team's, nor the reverse. A refused claim is paid for by a nonce bump and leaves the pot and its last claim epoch alone. As for moderation, state validation *is* the transform, so the mempool refuses with the same codes as a block.
The owner pot goes to the owner whole. The moderators pot is split equally between the team, and what the split leaves over, less than a credit per member, stays in the pot for the next claim, so no member is favoured by the order of the identity ids. Each pot is paid out at most once per epoch and the two are independent: the owner's claim does not use up the team's, nor the reverse. A refused claim is paid for by a nonce bump and leaves the pot and its last claim alone. As for moderation, state validation *is* the transform, so the mempool refuses with the same codes as a block.

The team is read when the claim executes. An owner who changes the appointed set by a contract update and then claims pays the new set: that follows from the owner controlling the contract's config, and is not prevented. The claim credits every recipient's balance, which is why a named moderator must exist (41110): crediting a balance that is not there is an internal error.

The proof of a claim's execution shows the pot with its last claim epoch and the balance of every recipient, which the prover and the verifier both read from the contract. `VerifiedContractFeeClaim` carries the contract id, the pot, that epoch, the credits left in the pot and the balances. A pot that was never claimed proves no claim; a later claim of the same pot verifies just the same, so the result is classified as affected state.
The proof of a claim's execution shows the pot with its last claim and the balance of every recipient, which the prover and the verifier both read from the contract. `VerifiedContractFeeClaim` carries the contract id, the pot, that last claim (epoch, block time, claimant), the credits left in the pot and the balances. A pot that was never claimed proves no claim; a later claim of the same pot verifies just the same, so the result is classified as affected state.

### Reading the Pots

- `getContractFeePots(contract_id, prove)`: both pots of the contract, each with its credits and its last claim: the epoch and the block time it was paid out in, and the identity that claimed.

The query always reads both pots, so its proof is one fixed path query (`Drive::contract_fee_pots_query`) that the prover and `Drive::verify_contract_fee_pots` build alike, with nothing in the request to get wrong. A pot nothing was paid into yet has no element and reads as zero credits, and a pot never paid out has no last claim, which is not a claim in epoch 0: a pot can have been paid out in epoch 0, so the last claim is a message of its own on the wire, unset when there is none, and the JavaScript fields (`lastClaimEpoch`, `lastClaimTimeMs`, `lastClaimantId`) are absent together. The proof says nothing about the contract itself, only about what is stored under its id, so the node refuses the query for a contract it does not hold before it proves anything, and a client that needs to know the contract exists fetches it.

A recipient reads the pots to decide whether a claim is worth its gas: the credits are what it would pay, and a last claim epoch equal to the current epoch means the claim would be refused (41111). A member of the team also reads there which member last claimed for the team, and when. The Rust SDK has `Fetch` and `FetchUnproved` impls for `ContractFeePots` (`platform::contract_fee_pots`, queried by the contract id), the wasm-sdk `getContractFeePots` and `contractClaimFees`, and the JavaScript SDK `contracts.feePots` and `contracts.claimFees`.

The claim's proof is verified against the contract, which names who the pot pays, and the team can change by a contract update. So every client fetches the contract again before a claim instead of trusting a cached copy: `ClaimContractFees` in the Rust SDK, `contractClaimFees` in the wasm-sdk, and the wasm-sdk's generic `broadcastAndWait` for a `ContractFeeClaim` built by hand, which falls back to the cached copy when that fetch fails, because the transition is already broadcast by then.

## Versioning Touchpoints

All in place for protocol version 14: `CONTRACT_VERSIONS_V6` makes config V2 the config of every new contract (`max_version` and `default_current_version` 2) and `validate_config_update` 2; `STATE_TRANSITION_SERIALIZATION_VERSIONS_V3` and `DRIVE_ABCI_VALIDATION_VERSIONS_V10` carry the transition's slots and `batch_state_transition.contract_moderation_gate`, and the contract update's basic structure moves to 2 to validate the declaration; `DRIVE_CONTRACT_METHOD_VERSIONS_V4` bumps `insert_contract` to 2 and adds the `moderation` table (its `update_contract` 2 belongs to token distribution and does nothing for moderation); `DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4` adds the converter slot and bumps `documents_batch_transition` to 1 for the sweep; `DRIVE_VERIFY_METHOD_VERSIONS` and `DRIVE_ABCI_QUERY_VERSIONS` gain their moderation tables; `SYSTEM_LIMITS_V4` gains `max_contract_moderators`, `max_contract_suspension_until` and `max_contract_moderation_reason_length`.

## What Is Not There Yet

Action fees on token transitions, a DAPI query and SDK methods for the fee pots and the claim, group-based moderators (`AuthorizedActionTakers::Group` through group actions), keys bound to the contract allowed to sign its moderation, ban codes declared by the contract (the reason's `code` is where they will go), further entry metadata such as a timestamp or the moderator's id, and the Swift and Kotlin SDKs. The refusal a barred identity receives (41107, 41108, 41114) does not repeat the reason: the status query does.
Action fees on token transitions, group-based moderators (`AuthorizedActionTakers::Group` through group actions), keys bound to the contract allowed to sign its moderation, ban codes declared by the contract (the reason's `code` is where they will go), further entry metadata such as a timestamp or the moderator's id, and the Swift and Kotlin SDKs. The refusal a barred identity receives (41107, 41108, 41114) does not repeat the reason: the status query does.

## Tests

Expand Down
6 changes: 4 additions & 2 deletions packages/dapi-grpc/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
// Derive features for versioned messages
//
// "GetConsensusParamsRequest" is excluded as this message does not support proofs
const VERSIONED_REQUESTS: [&str; 64] = [
const VERSIONED_REQUESTS: [&str; 65] = [
"GetContractModerationStatusRequest",
"GetContractModerationEntriesRequest",
"GetContractFeePotsRequest",
"GetContractGroupInfoRequest",
"GetContractGroupMembersRequest",
"GetContractGroupsForContractRequest",
Expand Down Expand Up @@ -165,9 +166,10 @@ fn configure_platform(mut platform: MappingConfig) -> MappingConfig {
// - "GetIdentityByNonUniquePublicKeyHashResponse"
//
// "GetEvonodesProposedEpochBlocksResponse" is used for 2 Requests
const VERSIONED_RESPONSES: [&str; 61] = [
const VERSIONED_RESPONSES: [&str; 62] = [
"GetContractModerationStatusResponse",
"GetContractModerationEntriesResponse",
"GetContractFeePotsResponse",
"GetContractGroupInfoResponse",
"GetContractGroupMembersResponse",
"GetContractGroupsForContractResponse",
Expand Down
Loading
Loading