diff --git a/pages/developers/_meta.json b/pages/developers/_meta.json index a289e997..4847f5d9 100644 --- a/pages/developers/_meta.json +++ b/pages/developers/_meta.json @@ -2,5 +2,6 @@ "networks": "Networks & RPCs", "intelligent-contracts": "Intelligent Contracts", "decentralized-applications": "Frontend & SDK Integration", - "staking-guide": "Staking Contract Guide" + "staking-guide": "Staking Contract Guide", + "error-reference": "Error & Revert Reference" } diff --git a/pages/developers/decentralized-applications/_meta.json b/pages/developers/decentralized-applications/_meta.json index 8bc04301..f585830e 100644 --- a/pages/developers/decentralized-applications/_meta.json +++ b/pages/developers/decentralized-applications/_meta.json @@ -5,6 +5,11 @@ "querying-a-transaction": "Querying a Transaction", "reading-data": "Reading Data from Intelligent Contracts", "writing-data": "Writing Data to Intelligent Contracts", + "fees-and-transaction-kit": "Fees and Transaction Policy", + "transaction-kit-integration": "Transaction Kit Integration", + "fee-profiling-and-estimation": "Fee Profiling and Estimation", + "fee-outcomes-and-debugging": "Fee Outcomes and Debugging", + "developer-nft-rewards": "Developer NFT Rewards", "testing": "Testing in Studio", "project-boilerplate": "Project Boilerplate" } diff --git a/pages/developers/decentralized-applications/developer-nft-rewards.mdx b/pages/developers/decentralized-applications/developer-nft-rewards.mdx new file mode 100644 index 00000000..14fd78d6 --- /dev/null +++ b/pages/developers/decentralized-applications/developer-nft-rewards.mdx @@ -0,0 +1,175 @@ +--- +description: "Developer NFT Rewards explains how GenLayer credits contract deployers with a share of transaction fees and epoch inflation, and how to read and claim those rewards with GenLayerJS." +--- + +import { Callout } from "nextra-theme-docs"; + +# Developer NFT Rewards + +GenLayer rewards the people who build on it. The first time you deploy an Intelligent Contract, the network mints a **Developer NFT** to your deploying address. That NFT accrues rewards from the activity your contracts generate, and you claim those rewards on-chain whenever you like. + +This page covers what the Developer NFT is, how it earns, and how to read and claim rewards with [GenLayerJS](/developers/decentralized-applications/genlayer-js). + + + The Developer NFT is minted automatically on your first contract deployment — there is no separate registration step. You get **one NFT per developer address**. + + +## What the Developer NFT is + +- It is minted to the **deployer** (the address that sends the deploy transaction) the first time that address deploys a contract. +- There is exactly **one NFT per developer address**. Deploying again from the same address does not mint a second NFT. +- The NFT is the account that accumulates and holds your claimable rewards. Only the NFT owner can claim. + +Each NFT tracks: + +| Field | Meaning | +|-------|---------| +| `nftId` | The on-chain id of your Developer NFT. | +| `developer` | The address that owns the NFT. | +| `claimableRewards` | Fee rewards accumulated so far and not yet claimed. | +| `lastClaimedEpoch` | The last epoch whose inflation rewards you have claimed. | +| `ghosts` | The contract ("ghost") addresses associated with this NFT. | + +## How rewards are earned + +Developer rewards come from two independent sources. + +### 1. Fee share + +Roughly **10% of the gross transaction fees** paid by transactions against your contract are credited to your NFT. This is the fee leg, and it is credited **when each transaction finalizes** — not at acceptance. + +- Credited per transaction, as soon as that transaction reaches finalization. +- No epoch has to close first — fee rewards become claimable immediately. +- Rewards are attributed to the epoch in which the transaction was **created**, which matters for the inflation share below. + +### 2. Inflation share + +Each epoch, GenLayer sets aside **10% of that epoch's inflation** into a pool for developers. That pool is split across Developer NFTs **pro-rata by the fees each NFT's contracts generated during the epoch**, with one important limit: + + + Your inflation reward for an epoch is **capped at 1× the fees your contracts earned that epoch**. If your contracts earned no fees in an epoch, you earn no inflation for that epoch — the unclaimed portion of the pool is burned. + + +In practice, on a low-traffic network the developer inflation pool is larger than the total fees generated, so each NFT typically receives an inflation reward roughly equal to its own fee earnings for the epoch (that is, up to a **2× effect**: your fee share plus a matching inflation share). + +### When rewards become claimable + +| Reward source | Claimable when | +|---------------|----------------| +| Fee share | Immediately, at each transaction's finalization. | +| Inflation share | Once the epoch is **finalized** (a finalized epoch lags the current epoch by at least one step). | + +The current, unfinalized epoch is never included in claimable inflation. + +## Reading and claiming with GenLayerJS + +All of the following use the standard GenLayerJS client. The NFT contract address is resolved automatically from the network's on-chain address registry — you don't pass it yourself. + +```typescript +import { testnetBradbury } from "genlayer-js/chains"; +import { createClient, createAccount } from "genlayer-js"; + +const account = createAccount(); // or createAccount(privateKey) +const client = createClient({ + chain: testnetBradbury, + account, +}); +``` + +### Look up your Developer NFT + +`getDeveloperNft` returns the full reward record for a developer address, or `null` if that address has never deployed a contract. + +```typescript +const nft = await client.getDeveloperNft({ developer: account.address }); + +if (nft === null) { + console.log("This address has no Developer NFT yet — deploy a contract first."); +} else { + console.log("NFT id:", nft.nftId); + console.log("Unclaimed fee rewards:", nft.claimableRewards); + console.log("Last claimed epoch:", nft.lastClaimedEpoch); + console.log("Contracts (ghosts):", nft.ghosts); +} +``` + +The returned shape is: + +```typescript +interface DeveloperNft { + nftId: bigint; + developer: `0x${string}`; + claimableRewards: bigint; + lastClaimedEpoch: bigint; + ghosts: `0x${string}`[]; +} +``` + +### Check claimable rewards + +Fee rewards and inflation rewards are read separately. + +```typescript +// All-time accumulated fee rewards not yet claimed. +const feeRewards = await client.getClaimableRewardsFromFees({ + nftId: nft.nftId, +}); + +// Inflation rewards over the next `numberOfEpochsToClaim` finalized epochs, +// starting from lastClaimedEpoch + 1. +const inflationRewards = await client.getClaimableRewardsFromInflation({ + nftId: nft.nftId, + numberOfEpochsToClaim: 50n, +}); + +console.log("Claimable from fees:", feeRewards); // bigint (wei) +console.log("Claimable from inflation:", inflationRewards); // bigint (wei) +``` + + + `getClaimableRewardsFromInflation` requires **two** arguments: the `nftId` and how many epochs to look ahead. It only counts epochs that are already finalized; the current epoch is excluded. + + +### Claim rewards + +There are two ways to claim. + +**Claim everything** — sweeps all accumulated fee rewards plus inflation for every finalized epoch: + +```typescript +const txHash = await client.claimNftRewards({ + nftId: nft.nftId, +}); +``` + +**Claim a bounded number of epochs** — useful when many epochs have accumulated: + +```typescript +const txHash = await client.claimNftEpochs({ + nftId: nft.nftId, + numberOfEpochsToClaim: 50n, +}); +``` + + + A single claim processes **at most 50 epochs of inflation** at a time. If more than 50 finalized epochs have accumulated since your last claim, use `claimNftEpochs` (or call the claim repeatedly) to clear them in batches. The read view `getClaimableRewardsFromInflation` does **not** enforce this 50-epoch cap, so it can report more than a single claim will actually pay out — size your claim accordingly. + + +Both claim methods return the EVM transaction hash. Only the NFT owner can claim; claiming when there is nothing to claim reverts. + +## Gotchas + + + **One NFT per developer.** With current behavior, only the **first contract you deploy** from an address accrues rewards for that NFT. Additional contracts deployed from the same address do not add to the NFT's earnings today. Deploy from a dedicated address if you want a contract's activity to be tracked on its own NFT. + + +- **Zero-fee epochs earn no inflation.** Because the inflation share is capped at 1× your fee earnings for the epoch, an epoch in which your contracts generated no fees pays no inflation — that share is burned, not carried forward. +- **Claims sweep all fee rewards at once.** Any claim (`claimNftRewards` or `claimNftEpochs`) empties your entire accumulated fee balance, even if you only meant to claim a few epochs of inflation. There is no partial fee claim. +- **Inflation lags fees.** Fee rewards are claimable the moment a transaction finalizes; inflation for an epoch only becomes claimable after that epoch is finalized, which is at least one step behind the current epoch. + +## Related + +- [GenLayer JS](/developers/decentralized-applications/genlayer-js) — client setup and configuration. +- [Reading Data from Intelligent Contracts](/developers/decentralized-applications/reading-data) +- [Writing Data to Intelligent Contracts](/developers/decentralized-applications/writing-data) +- [Staking Contract Guide](/developers/staking-guide) — the sibling reward system for validators and delegators. diff --git a/pages/developers/decentralized-applications/fee-outcomes-and-debugging.mdx b/pages/developers/decentralized-applications/fee-outcomes-and-debugging.mdx new file mode 100644 index 00000000..a7a02349 --- /dev/null +++ b/pages/developers/decentralized-applications/fee-outcomes-and-debugging.mdx @@ -0,0 +1,63 @@ +# Fee Outcomes and Debugging + +Applications should explain transaction outcomes in terms users or operators can act on. A submitted GenLayer transaction can be accepted by consensus, finalized later, rejected before execution or finish with a contract error. + +## Success rule + +A transaction is successful only when both are true: + +- the transaction status is `ACCEPTED` or `FINALIZED` +- the execution result is `FINISHED_WITH_RETURN` + +Use `isSuccessful(tx)` from `genlayer-js` or the transaction-kit outcome surfaces instead of checking status alone. A transaction can be accepted by validators and still finish with a contract error. + +## User-facing outcomes + +| Status and result | Meaning | Surface it as | +| --- | --- | --- | +| `ACCEPTED` / `FINALIZED` + `FINISHED_WITH_RETURN` | Execution succeeded. | Done. Unused fee budget refunds at finalization. | +| `UNDETERMINED` + any result | Validators could not reach a majority. | Treat as not executed, even if a leader result exists. | +| Rejected at submission, such as `MaxPriceExceeded` | A price cap or funding rule failed before execution. | Nothing charged. Re-estimate and retry. | +| `FINISHED_WITH_ERROR` | The contract reverted or errored. | Execution failed. Consensus fees may still be consumed. | + +## Gasless networks + +Some Studio deployments run gasless, with fee accounting disabled and all prices zero. The transaction kit detects this from the estimate: + +```typescript +if (quote.gasless) { + // Show "No fees on this network" and submit without fee params. +} +``` + +The same app code can work on gasless Studio and fee-charging networks. Do not hard-code a network name to decide whether fees are enabled; use the estimate result. + +## Refunds + +The initial deposit is a maximum budget, not the final cost. Unused budget is refunded when the transaction finalizes. Users should understand three numbers: + +- total deposit: what must be available when signing +- spent fees: what the transaction actually consumed +- refund: the unused part returned after finalization + +If an app tracks only until `decided`, it may not yet know the final refund. Use `finalized` tracking for screens or tools that need final fee settlement. + +## Explorer debugging + +Open the transaction in GenLayer Explorer and inspect the Fees section. It should show: + +- allocation versus consumed values +- locked price caps versus current prices +- message fee budget and consumption +- refunds by category +- settlement status + +If a quote looks too large, check whether the transaction used a developer profile or network defaults. `PolicyQuote.source` is `developer` when a matching `fee-profile.json` entry was used and `network-default` otherwise. + +If a transaction failed even though the user accepted the quote, check: + +- whether a price cap was exceeded before execution +- whether the method hit an unmeasured expensive branch +- whether `totalMessageFees` was too low for emitted child transactions +- whether the contract itself returned `FINISHED_WITH_ERROR` +- whether the app tracked only status and ignored execution result diff --git a/pages/developers/decentralized-applications/fee-profiling-and-estimation.mdx b/pages/developers/decentralized-applications/fee-profiling-and-estimation.mdx new file mode 100644 index 00000000..1fd69cca --- /dev/null +++ b/pages/developers/decentralized-applications/fee-profiling-and-estimation.mdx @@ -0,0 +1,330 @@ +# Fee Profiling and Estimation + +Fee profiling is how a GenLayer app turns contract tests into reusable fee suggestions. The output is a JSON file, usually `frontend/fee-profile.json` or `fee-profile.json`, that any submission path can use when it estimates a deploy or write transaction. + +The profile is not a live simulation result. It is a checked-in artifact generated from representative tests. Prices and caps are still read live at transaction time. + +`@genlayer/transaction-kit` can consume the profile directly for frontend flows. If you are building a CLI, backend worker, Python script or custom JS app without transaction-kit, use the same profile values as inputs to `estimateTransactionFees` / `estimate_transaction_fees`, then submit the returned `distribution` and `feeValue`. + +## Why profiles exist + +GenLayer transactions can take different paths: + +- a method may or may not emit messages +- a method may make different LLM or web calls depending on state +- a child transaction may terminate early or run through an expensive branch +- appeals and rotations change the amount of consensus work that must be funded +- storage, receipt and rollup writes vary with output size + +Simulating all of that before every user action would be too slow. Instead, the developer measures the important branches during tests, commits the maximum observed requirements and lets the app quote from that profile. + +## How methods are aggregated + +Profiles are keyed by contract method name, not by pytest test name and not by arguments. + +If five tests call `register_and_claim`, all five observations are merged into one `methods.register_and_claim` entry. For each field, the profiler keeps the maximum value observed across the whole run, then applies headroom to fee and time-unit values. + +Example: + +| Test path | Execution budget | Message fees | Rotations | +| --- | ---: | ---: | ---: | +| user already registered, no referral message | 600 | 0 | 0 | +| new user, emits referral message | 200 | 800 | 1 | + +The generated profile uses: + +```json +{ + "methods": { + "register_and_claim": { + "executionBudgetPerRound": "600", + "totalMessageFees": "800", + "rotationsPerRound": "1" + } + } +} +``` + +That combined entry may not describe a single historical transaction. It is intentionally conservative: it is the union of the worst observed components for that method. This is the right default for app safety because a user can usually trigger any branch that the method allows. + +If your app can prove a specific user flow cannot hit the expensive branch, use an explicit override for that flow. Keep the committed profile conservative. + +## What to measure + +Measure scenarios, not just methods. A good profile suite covers: + +- deploy, if the app lets users deploy contracts +- the cheapest successful path +- the most expensive normal path +- every branch that emits a different message set +- every branch that changes storage or receipt size materially +- every branch where child transactions can run different paths +- expected failure/revert paths, if users can trigger them and they consume fees + +For a Rally-style contract, that means tests for paths such as: + +- user already registered, no referral message +- user not registered, referral message emitted +- main transaction passes and emits the shard message +- main transaction fails before the shard message +- shard transaction terminates early +- shard transaction reaches the expensive branch + +The parent method profile should include the worst observed `totalMessageFees` across these paths. If the parent emits child transactions, the message fee budget must be large enough for the worst child path that the parent can create. + +## Generate the profile + +Create a script in the app repository so developers do not have to remember the exact command: + +```jsonc +{ + "scripts": { + "test:fees": "python3 -m pytest tests/integration/test_football_bets.py::test_fee_profile_create_bet --fee-profile frontend/fee-profile.json -v -s" + } +} +``` + +Run it against a fee-reporting Studio or network: + +```bash +npm run test:fees -- --rpc-url http://127.0.0.1:4000/api +``` + +The underlying mechanism is a pytest option provided by `gltest`: + +```bash +python3 -m pytest tests/integration --fee-profile frontend/fee-profile.json +``` + +Use `--fee-profile-headroom` to adjust the multiplier: + +```bash +python3 -m pytest tests/integration \ + --fee-profile frontend/fee-profile.json \ + --fee-profile-headroom 1.5 +``` + +The default headroom is `1.25`. + +## Wait for finalized receipts + +Profile tests should wait for finalized receipts: + +```python +receipt = contract.create_bet(args=["2024-06-20", "Spain", "Italy", "1"]).transact( + fees=transaction_fee_preset(), + wait_until="finalized", +) +``` + +Earlier statuses can prove that validators accepted a transaction before all fee accounting and refunds are settled. Finalized receipts are the reliable source for the profile. + +## Submit the profiling transaction + +The measured transaction itself still needs a fee preset. In local Studio runs, use a trusted developer preset sized from the active fee policy: + +```python +from gltest.clients import get_gl_client + +FEE_ESTIMATE_OPTIONS = { + "leaderTimeunitsAllocation": 100, + "validatorTimeunitsAllocation": 200, + "totalMessageFees": 0, + "rotations": [1], +} + +def transaction_fee_preset(): + estimate = get_gl_client().estimate_transaction_fees(FEE_ESTIMATE_OPTIONS) + return { + "distribution": estimate["distribution"], + "feeValue": estimate["feeValue"], + } +``` + +Use that preset on deploys and writes: + +```python +contract = factory.deploy(fees=transaction_fee_preset(), wait_until="finalized") + +receipt = contract.create_bet(args=["2024-06-20", "Spain", "Italy", "1"]).transact( + fees=transaction_fee_preset(), + wait_until="finalized", +) +``` + +For message-producing scenarios, increase `totalMessageFees` in the profiling preset so the emitted child transactions have enough budget. The generated profile records the observed message budget from the finalized receipt. If no message-producing path is measured, that method will recommend a zero message budget. + +## Profile shape + +A generated profile looks like this: + +```json +{ + "version": 1, + "network": "localnet", + "measuredAt": "2026-06-15T17:26:36Z", + "deploy": { + "leaderTimeunitsAllocation": "125", + "validatorTimeunitsAllocation": "250", + "executionBudgetPerRound": "786434", + "totalMessageFees": "0", + "rotationsPerRound": "1" + }, + "methods": { + "create_bet": { + "leaderTimeunitsAllocation": "125", + "validatorTimeunitsAllocation": "250", + "executionBudgetPerRound": "786297", + "totalMessageFees": "0", + "rotationsPerRound": "1" + } + } +} +``` + +| Field | Meaning | +| --- | --- | +| `leaderTimeunitsAllocation` | Suggested leader time units for the method or deploy. | +| `validatorTimeunitsAllocation` | Suggested validator time units for the method or deploy. | +| `executionBudgetPerRound` | Budget for receipt, storage and rollup writes per leader round. | +| `totalMessageFees` | Budget for child transactions emitted by the contract. | +| `rotationsPerRound` | Suggested rotations per leader round. This is recorded exactly, not multiplied by headroom. | + +The profile intentionally does not contain `feeValue` or live price caps. Those are quoted from the current network fee policy when the user is about to sign. + +## Use the profile in transaction-kit + +For frontend flows, commit the profile next to the frontend and pass it to the kit: + +```typescript +import feeProfile from './fee-profile.json'; + +const kit = createTransactionKit({ + chain, + provider, + account, + suggestions: feeProfile, +}); +``` + +When a transaction matches a profile entry, the quote source is `developer`. Methods without a matching entry fall back to network defaults. + +## Use the profile with genlayer-js + +Without transaction-kit, load the profile yourself, select the deploy or method entry, convert `rotationsPerRound` into the `rotations` array for the appeal posture you want, then estimate from live prices: + +```typescript +import feeProfile from './fee-profile.json'; + +const methodProfile = feeProfile.methods.create_bet; +const appealRounds = 1n; +const rotationsPerRound = BigInt(methodProfile.rotationsPerRound ?? '0'); + +const estimate = await client.estimateTransactionFees({ + leaderTimeunitsAllocation: BigInt(methodProfile.leaderTimeunitsAllocation), + validatorTimeunitsAllocation: BigInt(methodProfile.validatorTimeunitsAllocation), + executionBudgetPerRound: BigInt(methodProfile.executionBudgetPerRound), + totalMessageFees: BigInt(methodProfile.totalMessageFees ?? '0'), + appealRounds, + rotations: Array.from( + { length: Number(appealRounds) + 1 }, + () => rotationsPerRound, + ), +}); + +await client.writeContract({ + address: contract, + functionName: 'create_bet', + args: ['2024-06-20', 'Spain', 'Italy', '1'], + fees: { + distribution: estimate.distribution, + feeValue: estimate.feeValue, + }, +}); +``` + +## Use the profile with genlayer-py + +Python follows the same shape: + +```python +import json + +with open("fee-profile.json", encoding="utf-8") as f: + fee_profile = json.load(f) + +method_profile = fee_profile["methods"]["create_bet"] +appeal_rounds = 1 +rotations_per_round = int(method_profile.get("rotationsPerRound", "0")) + +estimate = client.estimate_transaction_fees( + { + "leaderTimeunitsAllocation": int(method_profile["leaderTimeunitsAllocation"]), + "validatorTimeunitsAllocation": int(method_profile["validatorTimeunitsAllocation"]), + "executionBudgetPerRound": int(method_profile["executionBudgetPerRound"]), + "totalMessageFees": int(method_profile.get("totalMessageFees", "0")), + "appealRounds": appeal_rounds, + "rotations": [rotations_per_round] * (appeal_rounds + 1), + } +) + +tx_hash = client.write_contract( + account=account, + address=contract_address, + function_name="create_bet", + args=["2024-06-20", "Spain", "Italy", "1"], + fees={ + "distribution": estimate["distribution"], + "feeValue": estimate["feeValue"], + }, +) +``` + +## Use the profile with the CLI + +The CLI can consume the generated profile directly: + +```bash +genlayer estimate-fees 0x123456789abcdef create_bet \ + --fee-profile frontend/fee-profile.json \ + --fee-preset standard \ + --json \ + --args "2024-06-20" "Spain" "Italy" "1" +``` + +For writes, pass the same profile path when submitting: + +```bash +genlayer write 0x123456789abcdef create_bet \ + --fee-profile frontend/fee-profile.json \ + --fee-preset standard \ + --args "2024-06-20" "Spain" "Italy" "1" +``` + +For deploys, the CLI uses the profile's `deploy` entry: + +```bash +genlayer deploy \ + --contract contracts/football_bets.py \ + --fee-profile frontend/fee-profile.json +``` + +`write` and targeted `estimate-fees` use `methods[method]`. The CLI converts the measured profile entry into SDK estimate options, reads current prices from the backend, then submits the returned `distribution` and `feeValue`. + +`--fee-preset` controls the appeal posture. The standard preset maps to one appeal round; `low` maps to zero and `high` maps to two. Use `--appeal-rounds` for an explicit override. + +Use `--fees` alongside `--fee-profile` only when a flow needs to override part of the generated profile, for example a custom `messageAllocations` set. Use `--fee-value` only when you need to force the transaction deposit instead of using the SDK estimate. + +This is intentionally the same policy flow as transaction-kit: the profile provides allocation suggestions, while the SDK/CLI estimate reads current network prices and produces the final deposit. + +## Regenerate policy + +Regenerate and review `fee-profile.json` when you change: + +- contract code that affects LLM calls, web requests, messages, storage or receipt data +- application arguments that select different branches +- child transaction behavior +- intended appeal or rotation posture +- the GenVM or Studio version used for fee accounting + +Treat the file like a performance budget. It should change in the same pull request as the behavior that changed the fee shape. diff --git a/pages/developers/decentralized-applications/fees-and-transaction-kit.mdx b/pages/developers/decentralized-applications/fees-and-transaction-kit.mdx new file mode 100644 index 00000000..dd8de0c3 --- /dev/null +++ b/pages/developers/decentralized-applications/fees-and-transaction-kit.mdx @@ -0,0 +1,47 @@ +# Fees and Transaction Policy + +GenLayer apps should treat fees as a measured application policy, not as ad hoc math at submission time. Whether you are building a web frontend, a backend service, a CLI or an automation script, the transaction must include fee parameters that are large enough for the path it can execute. + +The suggested fee parameters come from a developer fee profile generated by the contract test suite. `@genlayer/transaction-kit` is the recommended frontend integration, but the profile itself is not tied to transaction-kit. + +## The model + +Every transaction makes one deposit up front. The deposit covers: + +- consensus work: leader and validator time units, priced in GEN per time unit +- execution budget: receipt, storage and rollup writes +- message budget: child transactions emitted by the contract +- user value: any value intentionally sent to the contract + +Unused fee budget is refunded automatically when the transaction finalizes. Price caps are locked at submission time; if network prices rise above a cap before execution, the transaction is rejected and nothing is charged. + +## The pieces + +| Page | Use it for | +| --- | --- | +| [Transaction Kit Integration](./transaction-kit-integration) | Add the fee approval, signing and tracking flow to a React, Vue or headless frontend. | +| [Fee Profiling and Estimation](./fee-profiling-and-estimation) | Measure deploy and method fee requirements from representative contract tests and use `fee-profile.json` from JS, Python, CLI or transaction-kit. | +| [Fee Outcomes and Debugging](./fee-outcomes-and-debugging) | Interpret transaction outcomes, gasless networks, refunds and Explorer fee panels. | + +## Recommended workflow + +1. Write normal contract tests for the app paths users can trigger. +2. Add fee-profile coverage for each economically meaningful branch. +3. Run the profile tests against a fee-reporting Studio or network. +4. Commit the generated `fee-profile.json` where your app or tool can load it. +5. Use the profile entry for the deploy or method you are submitting. +6. Estimate with the current network fee policy and submit the returned `distribution` and `feeValue`. + +This avoids live simulation before every user transaction. GenLayer transactions can include LLM calls, web requests, appeals and emitted messages, so per-click simulation would make the app slow and expensive. The profile gives the app a reproducible, test-backed estimate; the SDK still reads current prices and caps from the network when the user is about to sign. + +## Tooling + +| Tool | Role | +| --- | --- | +| `genlayer-js` v2 | SDK methods for fee estimation, deploy/write submission, receipt waiting and success checks. | +| `genlayer-py` | Python SDK methods for the same estimate and submit flow in scripts, services and CLIs. | +| `@genlayer/transaction-kit` | Core estimate, submit, track and verification flow. | +| `@genlayer/transaction-kit-react` / `-vue` | Drop-in UI for fee receipt, preset selection, price protection, signing and status tracking. | +| GenLayer CLI | `estimate-fees`, `deploy` and `write` commands that accept `fee-profile.json` for terminal workflows. | +| `gltest --fee-profile` | Measures fee usage from contract tests and writes the profile consumed by apps and tools. | +| GenLayer Explorer | Shows transaction fee allocation, consumption, refunds and settlement details. | diff --git a/pages/developers/decentralized-applications/transaction-kit-integration.mdx b/pages/developers/decentralized-applications/transaction-kit-integration.mdx new file mode 100644 index 00000000..61be5ed0 --- /dev/null +++ b/pages/developers/decentralized-applications/transaction-kit-integration.mdx @@ -0,0 +1,122 @@ +# Transaction Kit Integration + +`@genlayer/transaction-kit` is the frontend layer for GenLayer fee approval and transaction tracking. It turns a transaction plus optional developer fee profile into a quote, shows the user what they are funding, submits through an EIP-1193 wallet provider and tracks the result. + +## Install + +```jsonc +{ + "dependencies": { + "@genlayer/transaction-kit": "github:genlayerlabs/genlayer-transaction-kit#pkg/core", + "@genlayer/transaction-kit-react": "github:genlayerlabs/genlayer-transaction-kit#pkg/react" + } +} +``` + +Vue apps use `#pkg/vue`. The adapters are thin UI packages around the same core flow. + +## Create the kit + +The kit depends on an injected [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) provider. MetaMask, Privy embedded wallets, external Privy wallets, WalletConnect and similar wallets work as long as they expose `request()`. + +```typescript +import { createTransactionKit } from '@genlayer/transaction-kit'; +import { testnetAsimov } from 'genlayer-js/chains'; +import feeProfile from './fee-profile.json'; + +const kit = createTransactionKit({ + chain: testnetAsimov, + provider: window.ethereum, + account: userAddress, + suggestions: feeProfile, +}); +``` + +`suggestions` is optional. Without it, the kit falls back to network defaults. With it, matching methods are quoted from your measured developer fee profile while prices and caps still come from live network reads. + +## Use the React panel + +```tsx +import { GenLayerTransactionPanel } from '@genlayer/transaction-kit-react'; +import '@genlayer/transaction-kit-react/styles.css'; + + refetchAppState()} +/> +``` + +The panel owns the normal flow: + +1. estimate the fee policy +2. show an itemized fee receipt +3. let the user choose `low`, `standard` or `high` appeal posture +4. show price-protection caps +5. ask the wallet to sign +6. track the transaction +7. surface success, failure or an unresolved outcome + +Deploys use the same panel shape: + +```tsx + +``` + +## Use the headless flow + +Use the headless API when you need a custom UI: + +```typescript +const quote = await kit.estimate( + { preset: 'standard' }, + { kind: 'write', address: contract, method: 'place_bet', args: [42] }, +); + +const { genlayerTxId } = await kit.submit(quote, { + kind: 'write', + address: contract, + method: 'place_bet', + args: [42], +}); + +const final = await kit.track(genlayerTxId, (status) => { + console.log(status.phase, status.statusName, status.executionResultName); +}); +``` + +The same flow is also exposed through framework hooks/composables where available. + +## Presets, profiles and overrides + +The kit builds fee options in this order: + +1. user preset: `low`, `standard` or `high` +2. developer profile: measured method or deploy suggestions +3. explicit caller overrides + +Use the preset for the user's appeal posture. Use the profile for method-specific allocations, message budget and rotations. Use overrides only when the app deliberately knows more than the default profile for a specific flow. + +```typescript +await kit.estimate( + { + preset: 'high', + overrides: { + totalMessageFees: 10_000_000_000_000_000n, + }, + }, + { kind: 'write', address: contract, method: 'register_and_claim', args }, +); +``` + +Overrides are useful for advanced apps that can distinguish modes before submission. For example, if a method has a rare expensive branch and the app can prove the current user cannot hit it, the app may override a lower message budget. The default committed profile should still be conservative. + +## Long-running transactions + +Do not force users to stare at a modal for minutes. Transactions that call LLMs, fetch web data or wait through appeals can take time. Use the panel timeline for immediate feedback, but track the transaction in your backend or application state as well. A user should be able to navigate away and come back to the outcome. diff --git a/pages/developers/error-reference.mdx b/pages/developers/error-reference.mdx new file mode 100644 index 00000000..5a3a5a6c --- /dev/null +++ b/pages/developers/error-reference.mdx @@ -0,0 +1,351 @@ +--- +description: "A reference catalog of the GenLayer consensus protocol's custom Solidity errors: each error's 4-byte selector, its meaning, and the common cause or fix, organized by area, plus common encoding pitfalls." +--- + +import { Callout } from 'nextra-theme-docs' + +# Error & Revert Reference + +When a GenLayer protocol contract rejects a call, it reverts with a **custom error** rather than a plain string. Tooling that does not have the contract ABI shows you only the raw revert data — a 4-byte **selector** such as `0x632be5a1`, sometimes followed by ABI-encoded arguments. This page lets you translate that selector back into a named error, understand what it means, and see the most common cause or fix. + +## How to Look Up a Selector + +The first four bytes of a revert payload identify the error. For example, a revert that begins `0x632be5a1` corresponds to `FeeValueMustBeNonZero(uint256)` — a required fee field was left at zero. + +- If you have the full revert data, take the first four bytes (the leading `0x` plus eight hex characters) and find it in the tables below. +- The selector is derived as the first four bytes of `keccak256("ErrorName(type1,type2,...)")`, using the argument **types only** — no parameter names, no spaces. Any tool that computes function/error selectors (for instance, `cast 4byte-decode` or `cast sig`) uses the same rule. +- Errors that carry arguments (for example `BatchSizeExceeded(uint256,uint256)`) encode those values immediately after the selector; the parenthesized note in the **Meaning** column tells you what each argument is. + + +Selectors are computed from the exact error signature. `FeeValueMustBeNonZero(uint256)` and a hypothetical `FeeValueMustBeNonZero()` are *different* selectors. When computing a selector yourself, always use the canonical, type-only signature and omit parameter names — hashing a signature that still contains parameter names produces a wrong selector. + + +## Common Encoding Pitfalls + +Some reverts you hit while integrating are not protocol errors at all, but encoding mistakes in the request you sent. A few recurring ones: + +- **Write-transaction calldata must be wrapped.** The `data` for a write (state-changing) transaction is not the raw contract calldata. It must be `rlp.encode([calldata, leader_only])` — the calldata together with the leader-only flag. Passing the raw calldata directly typically surfaces as an RLP decoding error like **`RLP string ends with superfluous bytes`**. If you see that message, wrap your calldata as an RLP list rather than sending it bare. +- **Selector present but arguments missing or misaligned.** If a selector matches an error that takes arguments but the payload is only four bytes long (or the wrong length), the sending code likely built the error data by hand. Decode the arguments against the signature shown in the tables. +- **Wrong signature when decoding.** If a selector does not appear in these tables, double-check that you are hashing the type-only signature. A mismatch between your assumed signature and the real one yields a selector that will never resolve here. + +## How This Catalog Was Generated + +The error signatures are taken directly from the consensus repository's canonical list (`scripts/utils/errorSelectors.ts` on the `v0.6-dev` line), and each 4-byte selector is computed from the type-only signature with `keccak256`. The list was validated against the known anchor `FeeValueMustBeNonZero(uint256)` = `0x632be5a1`, and checked to contain no selector collisions. The meanings and fixes are derived from the errors' definitions and revert sites in the contracts; where a name did not permit a confident gloss, the entry is deliberately conservative. + + +This reference reflects a development snapshot of the consensus contracts and is intended for orientation, not as a compatibility guarantee. Error names, selectors, and especially the exact conditions that trigger them can change between protocol versions. For a definitive decode, always match against the ABI of the specific deployed contract you are calling. Entries noted as "(test mock)" originate from test-only contracts and should not be expected on a production network. + + +## Error Tables + +The catalog is grouped by protocol area and sorted alphabetically within each group for lookup. Every entry lists the error signature, its 4-byte selector, a one-line meaning, and the most common cause or fix. + +### Fees (39) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `AllocationCommitmentMissing()` | `0xbe77b500` | Expected fee-allocation commitment is not present | Include the commitment | +| `AllocationDuplicateKey()` | `0xbb598087` | Two sibling allocations share (messageType, recipient, callKey) | Deduplicate allocation keys | +| `AllocationLifecycleBudgetInsufficient()` | `0x00583b33` | Budget below minPrimary times (appealRounds+1) for on-acceptance | Raise on-acceptance budget | +| `AllocationRestrictedTxCannotAcceptMessageFees()` | `0x35ebdf50` | A restricted-allocation tx illegally accepted message fees | Do not accept message fees here | +| `AllocationSubtreeHashMismatch()` | `0xdda17044` | Provided subtree hash differs from committed hash | Provide the committed subtree | +| `AllocationSubtreeRequired()` | `0x47db53f3` | Leader must carry subtree under HashCommitments but did not | Include required subtree | +| `AllocationTreeBudgetInconsistent()` | `0x328bfb08` | Allocation-tree node budgets do not sum consistently | Fix tree budget sums | +| `AllocationTreeMalformed()` | `0x7fecd5b1` | Fee allocation tree is structurally invalid | Rebuild a valid tree | +| `AllocationTreeTooDeep()` | `0x48668d66` | Allocation tree exceeds maximum depth | Flatten/reduce tree depth | +| `AlreadyClaimed()` | `0x646cf558` | Fee or reward already claimed | No double claim | +| `AlreadySettled()` | `0x560ff900` | Fees or transaction already settled | No re-settlement | +| `BudgetTooLow()` | `0x305e533c` | Provided budget is below the minimum required | Increase the budget | +| `ExecutionBudgetExceeded(uint256,uint256)` | `0x57df8523` | Execution gas consumption exceeded the tx budget (attempted, budget) | Raise execution budget | +| `ExternalBudgetInvalid()` | `0x8b434eea` | External message budget is invalid | Ensure budget >= gasLimit*price | +| `ExternalGasLimitBelowMinimum()` | `0xf5963b64` | External message gas limit below minimum | Raise external gas limit | +| `ExternalGasLimitOrPriceZero()` | `0xbf35c24e` | External message gas limit or price is zero | Provide non-zero limit and price | +| `ExternalMessageFreezeExceeded(bytes32,uint256,uint256)` | `0x05684868` | External message froze more value than allowed (txId, declaredValue, availableLimit) | Reduce declared value | +| `ExternalOnAcceptanceNotSupported()` | `0xa54ce0b6` | On-acceptance semantics not supported for external messages | Do not use on-acceptance externally | +| `FailedFeeTransfer()` | `0x24a8ac13` | Fee ETH transfer failed | Check recipient/balance | +| `FeeSettlementNotCompleted()` | `0x1432a761` | Operation requires fee settlement to complete first | Settle fees before proceeding | +| `FeeValueMustBeNonZero(uint256)` | `0x632be5a1` | A required fee field was zero (field index) | Provide a non-zero fee value | +| `InsufficientFees()` | `0x8d53e553` | Total fees provided do not cover the operation | Provide more fees | +| `InsufficientFeesForRound()` | `0x0c35bf69` | Fees insufficient to fund a specific consensus round | Increase per-round fees | +| `InsufficientGasForInternalMessageCreation(uint256,uint256,uint256,address)` | `0xb2c9bc63` | Outer tx lacks gas to forward per-child floor under EIP-150 (floor, required, available, recipient) | Raise outer gas limit | +| `InsufficientValue()` | `0x11011294` | msg.value is less than fees owed | Send more value | +| `MaxPriceExceeded(uint256,uint256)` | `0xb4132db3` | Global gas price exceeds the user's max-price cap (globalPrice, userMax) | Raise max price or retry cheaper | +| `MessageAllocationBudgetInsufficient()` | `0x67d6a6b3` | Allocation's budget is too low for the message | Increase allocation budget | +| `MessageAllocationsNotEqualBudget()` | `0x9659e275` | Sum of message allocations does not equal declared budget | Make allocations sum to budget | +| `MessageBudgetExceeded(uint256,uint256)` | `0xd67553bd` | Internal message consumed more than its budget (attempted, budget) | Raise message budget | +| `MessageDeclaredBudgetInsufficient()` | `0x7f295162` | Declared message budget is below required | Increase declared budget | +| `MessageEmissionPhaseMismatch()` | `0x5ca1f1d8` | message.onAcceptance differs from allocation.onAcceptance | Align emission phase flags | +| `MessageFeeParamsMismatch()` | `0x67b02b3b` | Message fee parameters do not match allocation/commitment | Align fee params | +| `MessageFeesReportMismatch()` | `0x86515990` | Reported per-message fees differ from expected | Correct the fee report | +| `MessageFeesTotalMustBeNonZero()` | `0xf79991cb` | Sum of message fees is zero when it must be positive | Provide non-zero message fees | +| `MessageNoMatchingAllocation()` | `0x4e4b3c38` | An emitted message has no matching fee allocation | Add a matching allocation | +| `OnlyConsensusCanCall()` | `0x6c6fe28a` | Fee-management function callable only by consensus | Only consensus may call | +| `OnlyFeeManager()` | `0x8f1dbd6c` | Caller is not the fee manager | Only fee manager may call | +| `RollupBudgetBelowFloor()` | `0xa70732ee` | Rollup/L1 gas budget is below the enforced floor | Increase rollup budget | +| `TooManyMessages()` | `0x1ec0b2f7` | Number of messages exceeds the allowed maximum | Emit fewer messages | + +### Staking (114) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `AccountsArrayEmpty()` | `0xb1d0b181` | GEN distribution accounts array is empty | Provide at least one account | +| `AllValidatorsConsumed()` | `0x54055ab9` | All validators have been consumed (test mock) | Wait for next selection | +| `AlreadyRevoked()` | `0x905e7107` | Vesting already revoked | No re-revocation | +| `AlreadyUnlocked()` | `0x5090d6c6` | Vesting tokens already unlocked | No re-unlock | +| `AmountMustBeGreaterThan0()` | `0x35f61689` | Amount must be greater than zero | Send a positive amount | +| `BurnTransferFailed()` | `0xaac1169b` | Burn transfer failed | Check burn path/balance | +| `CanOnlyTriggerInflationMaxEpochsInFuture()` | `0xdfaaa5b2` | Inflation trigger is too far in the future | Trigger within the allowed horizon | +| `CanOnlyTriggerInflationMaxTenEpochsInFuture()` | `0xa9f9ed6c` | Inflation trigger limited to at most ten epochs ahead | Trigger within ten epochs | +| `DeepthoughtCallFailed()` | `0xbd5ac82f` | Call to the Deepthought contract failed | Check Deepthought address/target | +| `DelegatorBelowMinimumStake()` | `0x944516fc` | Delegator stake is below the minimum | Increase delegation | +| `DelegatorExitExceedsShares()` | `0x64d3ea58` | Exit amount exceeds the delegator's shares | Exit at most held shares | +| `DelegatorExitWouldBeBelowMinimum()` | `0x5f9ff6b2` | Partial exit would drop stake below minimum | Exit fully or less | +| `DelegatorMayNotExitWithZeroShares()` | `0x161fa299` | Cannot exit with zero shares | Hold shares before exiting | +| `DelegatorMayNotJoinTwoValidatorsSimultaneously()` | `0x48a6b5ba` | Delegator may back only one validator at a time | Exit current validator first | +| `DelegatorMayNotJoinWithZeroValue()` | `0xba5cb6d8` | Delegation requires non-zero value | Send a non-zero amount | +| `DelegatorMustExitAllWhenBelowMinimum()` | `0x44d4da44` | Must fully exit when below minimum stake | Perform a full exit | +| `DeveloperAlreadyHasNFT()` | `0xa74b28d1` | Developer already holds a reward NFT | One NFT per developer | +| `DeveloperCannotBeZeroAddress()` | `0x7e3f46bd` | Developer address is the zero address | Provide a valid developer | +| `DeveloperHasNoNFT()` | `0x70849322` | Developer holds no reward NFT | Mint/assign an NFT first | +| `DeveloperHasNoRewards()` | `0xdf6b47b0` | Developer has no claimable rewards | Nothing to claim | +| `EpochAdvanceNotReady()` | `0xe7295fed` | Conditions to advance the epoch are not met | Wait until advance conditions hold | +| `EpochAlreadyFinalized()` | `0x3366263c` | Epoch is already finalized | No re-finalization | +| `EpochNotFinalized()` | `0x8cf75707` | Epoch is not finalized | Finalize the epoch first | +| `EpochNotFinished()` | `0xec766557` | Epoch has not finished yet | Wait for epoch end | +| `FacetCallFailed(bytes)` | `0xcf582143` | Diamond facet delegatecall failed (reason) | Inspect returned reason | +| `FailedTransfer(address)` | `0x3f32e1dd` | Token/ETH transfer to a validator failed (validator) | Check recipient/balance | +| `FailedTransferCall()` | `0x3825f587` | Reward/transfer call failed | Check recipient/balance | +| `FunctionNotFound(bytes4)` | `0x5416eb98` | No diamond facet implements this selector | Add/register the facet | +| `FundingMismatch()` | `0xb84a1afb` | Funding amount does not match expected | Fund the exact expected amount | +| `GhostAlreadyHasNFT()` | `0x36bd0868` | Ghost contract already has an NFT | One NFT per ghost | +| `GhostCannotBeZeroAddress()` | `0x4d8d14c5` | Ghost contract address is zero | Provide a valid ghost address | +| `IncentivePercentageTooHigh()` | `0xaf188845` | Incentive percentage exceeds the allowed cap | Lower the incentive percentage | +| `InflationAlreadyInitialized()` | `0x9ded3b15` | Inflation already initialized | Initialize once only | +| `InflationAlreadyReceived()` | `0x719a0d39` | Inflation for this period already received | No double receipt | +| `InflationInitialized()` | `0x067f34cf` | Inflation-initialized guard tripped | Operation not allowed post-init | +| `InflationInvalidAmount()` | `0x3c1f1f16` | Inflation amount is invalid | Provide a valid amount | +| `InflationNotReadyToBeRealized()` | `0x7d8fa225` | Inflation is not ready to be realized | Wait until realizable | +| `InflationRequestFailed()` | `0xced05c45` | Cross-layer inflation request failed | Check bridge/L2 path | +| `InitialMintAlreadyCalled()` | `0xacf9028d` | GEN initial mint already executed | Mint once only | +| `InsufficientBondCustody()` | `0xebab1869` | Bond custody balance is insufficient | Ensure sufficient bond custody | +| `InsufficientContractBalance()` | `0x786e0a99` | Vesting contract lacks sufficient balance | Fund the contract | +| `InsufficientInflationFunds()` | `0x58d77d2a` | Not enough funds to pay inflation | Fund inflation reserve | +| `InvalidAtEpoch()` | `0x90dce792` | Operation is invalid at the current epoch | Retry at an allowed epoch | +| `InvalidBanPeriod()` | `0xa2fc7bbd` | Ban period parameter is invalid | Provide a valid ban period | +| `InvalidCliffUnlockBps()` | `0x7f2d8c3d` | Vesting cliff unlock basis points invalid | Use bps within 0..10000 | +| `InvalidInflationThresholds()` | `0x4774d828` | Inflation threshold config is invalid | Fix inflation thresholds | +| `InvalidL2GasParams()` | `0x21e1b5c9` | L2 gas parameters are invalid | Provide valid L2 gas params | +| `InvalidNumberOfEpochsToClaim()` | `0xb9a8ce44` | NFT reward epoch-count to claim is invalid | Use a valid epoch count | +| `InvalidOperatorAddress()` | `0xeb32d3bf` | Operator address is invalid or zero | Provide a valid operator | +| `InvalidPeriodDuration()` | `0x9e11b5e6` | Vesting period duration is invalid | Provide a valid duration | +| `InvalidRecipient()` | `0x9c8d2cd2` | Reward/transfer recipient is invalid | Provide a valid recipient | +| `InvalidSlashPercentage()` | `0x37814740` | Slash percentage is invalid | Use a valid slash percentage | +| `L2BaseGasCostQueryFailed()` | `0x7dc9c5e2` | Failed to query L2 base gas cost | Check L2 gas oracle/bridge | +| `L2MessageAlreadyInvoked()` | `0x21d83750` | L2 message already invoked (replay guard) | Do not re-invoke | +| `L2MessageProvenFailed()` | `0xfd0ae327` | L2 message proof verification failed | Provide a valid proof | +| `L2TransactionRequestFailed()` | `0x1efae811` | L2 transaction request failed | Check L2 bridge/params | +| `ManualUnlockNotRequired()` | `0x90173285` | Manual unlock is not required here | Use standard vesting flow | +| `MaxNumberOfValidatorsReached()` | `0x9ce3911d` | Validator set is at capacity | Cannot add more validators | +| `MaxValidatorsCannotBeZero()` | `0x83c27a2d` | maxValidators config cannot be zero | Set a positive maxValidators | +| `NFTMinterCallFailed()` | `0x64c31e4b` | Call to the NFT minter failed | Check NFT minter address/target | +| `NFTMinterNotConfigured()` | `0xa0c98f30` | NFT minter address is not configured | Configure the NFT minter | +| `NoBurning()` | `0x96191d45` | Burning is not enabled or not applicable | Burning disabled in this config | +| `NoPendingOperator()` | `0x9c2af11f` | No pending operator transfer to finalize | Initiate a transfer first | +| `NoPreviousEpoch()` | `0x9fa56a5b` | No previous epoch exists (test mock) | Only valid after epoch 0 | +| `NotBeneficiary()` | `0x644d871f` | Caller is not the vesting beneficiary | Call from the beneficiary address | +| `NotCreator()` | `0x93687c0b` | Caller is not the vesting schedule creator | Call from the creator address | +| `NotEnoughValidators()` | `0xae575a88` | Not enough validators available (test mock) | Register more validators | +| `NotNFTOwner()` | `0x4088c61c` | Caller does not own the NFT | Call from the NFT owner | +| `NotRevocable()` | `0x9414820d` | Vesting schedule is not revocable | Cannot revoke this schedule | +| `NotRevoked()` | `0x73f7ab1e` | Vesting is not revoked but operation requires revoked state | Revoke first | +| `NotRevoker()` | `0x2ad3d44f` | Caller is not the vesting revoker | Call from the revoker address | +| `NoValidatorsAvailable()` | `0xc4e41c46` | No validators are available (test mock) | Register/activate validators | +| `NumberOfValidatorsExceedsAvailable()` | `0x9c637db9` | Requested count exceeds available validators | Request no more than available | +| `OnlyGEN()` | `0x6a10007b` | Callable only by the GEN token contract | Only GEN may call | +| `OnlyIdleness()` | `0xf2c0764c` | Callable only by the idleness module | Only idleness may call | +| `OnlyIdlenessOrTribunal()` | `0xfcde63e9` | Callable only by idleness or tribunal | Restricted to idleness/tribunal | +| `OnlyStakingContract()` | `0xd807afce` | Callable only by the staking contract | Only staking may call | +| `OnlyTransactions()` | `0x516257a3` | Callable only by the transactions module | Only transactions may call | +| `OnlyTransactionsOrTribunal()` | `0x6c4db06b` | Callable only by transactions or tribunal | Restricted to transactions/tribunal | +| `OnlyTribunal()` | `0x811befe9` | Callable only by the tribunal | Only tribunal may call | +| `OperatorAlreadyAssigned()` | `0x5acd21ba` | Operator address already assigned to a validator | Use a free operator address | +| `OperatorTransferNotReady()` | `0xde4e791a` | Operator transfer timelock not yet elapsed | Wait for the transfer window | +| `PendingTribunals(uint256)` | `0x773c68a6` | Epoch has unresolved tribunals blocking finalization (epoch) | Resolve tribunals first | +| `PreviousEpochNotFinalizable()` | `0x93b7eb86` | Previous epoch cannot be finalized yet | Finalize prior epoch prerequisites | +| `ReductionFactorCannotBeZero()` | `0x2e980407` | Burn/inflation reduction factor is zero | Set a non-zero factor | +| `SlashGovernanceDelayPassed()` | `0x06de1175` | Governance slash delay window has passed | Act within the slash window | +| `SlashPercentageTooHigh()` | `0x0edf5154` | Slash percentage exceeds the cap | Lower the slash percentage | +| `SlashRevokedToRemoveTooHigh(uint256,uint256)` | `0x217f3b2e` | Amount to remove exceeds revoked stake (revoked, toRemove) | Remove at most revoked amount | +| `TotalDistributionMustBe100()` | `0x16904932` | GEN distribution percentages must sum to 100 | Fix distribution to total 100 | +| `TransferFailed()` | `0x90b8ec18` | Token or ETH transfer failed | Check recipient/balance | +| `UnauthorizedDelegatorClaim()` | `0xdcc541d1` | Caller not authorized to claim delegator rewards | Claim from the entitled address | +| `UnauthorizedInflationRequest()` | `0x7d8f3b9e` | Caller not authorized to request inflation | Only authorized caller may request | +| `UnknownGenAction()` | `0xe43351e0` | Unknown GEN cross-layer action code | Use a recognized action code | +| `ValidatorAlreadyInTree()` | `0x45be71b6` | Validator is already in the selection tree | Do not re-insert | +| `ValidatorAlreadyJoined()` | `0x71d16bc6` | Validator has already joined | No re-join | +| `ValidatorBelowMinimumStake()` | `0x0b294dc3` | Validator stake is below the minimum | Top up validator stake | +| `ValidatorDoesNotExist()` | `0xe51315d2` | No such validator | Reference an existing validator | +| `ValidatorExitExceedsShares()` | `0xfddb7740` | Exit amount exceeds the validator's shares | Exit at most held shares | +| `ValidatorMayNotBeDelegator()` | `0x359b3ac0` | A validator may not also be a delegator | Separate the roles | +| `ValidatorMayNotDepositZeroValue()` | `0xffb117c5` | Validator deposit requires non-zero value | Deposit a positive amount | +| `ValidatorMayNotJoinWithZeroValue()` | `0xd25ef26f` | Validator join requires non-zero stake | Join with a positive stake | +| `ValidatorMustNotBeDelegator()` | `0x85d35a02` | An address cannot be both validator and delegator | Separate the roles | +| `ValidatorNotActive()` | `0xa6ce15f6` | Validator exists but is not active this epoch | Wait until validator is active | +| `ValidatorNotInTree()` | `0x8ee72f3f` | Validator is not in the selection tree | Insert validator first | +| `ValidatorNotJoined()` | `0xffc673e8` | Validator has not joined | Join before this action | +| `ValidatorsConsumed()` | `0xeae94a56` | All validators for this round already consumed | Wait for next round/selection | +| `ValidatorsUnavailable()` | `0xd0b5c3bb` | Validators are temporarily unavailable | Retry when validators are available | +| `ValidatorWithdrawalExceedsStake()` | `0xfb7f2a7f` | Withdrawal exceeds the staked amount | Withdraw at most staked | +| `VestingAlreadyExists()` | `0xe7075d2a` | A vesting schedule already exists for the target | Do not recreate the schedule | +| `VestingAlreadyStopped()` | `0xd731022d` | Vesting is already stopped | No re-stop | +| `VestingDeploymentFailed()` | `0x0f5cc9de` | Vesting contract deployment failed | Check beacon/factory params | +| `VestingNotStopped()` | `0x6e32bb06` | Vesting is not stopped but operation requires stopped state | Stop vesting first | +| `WithdrawExceedsVested()` | `0x8b6a4865` | Withdrawal exceeds the vested amount | Withdraw at most vested | +| `ZeroAmount()` | `0x1f2a2005` | Amount is zero | Provide a non-zero amount | + +### Consensus (100) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `AddingTransactionToUndeterminedQueueFailed()` | `0x5bd3cd8c` | Failed to add tx to the undetermined queue | Internal queue operation failed | +| `AllValidatorsCommitted()` | `0xb467acd8` | All validators have committed (none left) | No further commits expected | +| `AppealBondTooLow()` | `0xb44cda7b` | Appeal bond is below the required amount | Increase appeal bond | +| `AppealNotActive()` | `0x6565b6bf` | No appeal is currently active | Only valid during an active appeal | +| `AppealNotAllowed()` | `0xb94e4c42` | Appeal not permitted in the current state | Tx/round not appealable now | +| `AppealRoundAlreadyExists()` | `0x93a19b27` | An appeal round already exists | Do not re-create the round | +| `AppealRoundNotPermitted()` | `0x6ecc8d59` | This appeal round is not permitted | Round count/config disallows it | +| `BeaconAlreadyDeployed()` | `0xa1900dfe` | Beacon proxy already deployed | Do not redeploy the beacon | +| `BeaconNotDeployed()` | `0x261438ca` | Beacon proxy not deployed yet | Deploy the beacon first | +| `CallerNotActivator()` | `0xb56aa94e` | Caller is not the activator | Call from the activator | +| `CallerNotConsensus()` | `0x47820187` | Caller is not the consensus contract | Only consensus may call | +| `CallerNotGenConsensus()` | `0xf8beed7d` | Caller is not GenConsensus | Only GenConsensus may call | +| `CallerNotLeader(address,address)` | `0x3558c9da` | Caller is not the round leader (leader, caller) | Only the current leader may call | +| `CallerNotMessages()` | `0x7e3d1f98` | Caller is not the messages module | Only messages module may call | +| `CallerNotSender()` | `0xf5963d65` | Caller is not the original tx sender | Only the sender may call | +| `CallerNotTransactions()` | `0xae49b478` | Caller is not the transactions module | Only transactions module may call | +| `CanNotAppeal()` | `0xb39cdfbe` | The transaction/state cannot be appealed | Not eligible for appeal | +| `EmptyTransaction()` | `0x260c9d62` | Transaction payload is empty | Provide a non-empty tx | +| `FinalizationNotAllowed()` | `0xe1b3b3b7` | Finalization is not allowed at this time | Wait until finalization is permitted | +| `FinalizationWindowForRevealingNotOpened()` | `0xabc07e66` | Reveal finalization window is not open | Wait for the reveal window | +| `FinalizedCountExceedsIssued()` | `0x34bffaee` | Finalized count exceeds issued count | Internal accounting invariant broke | +| `IdlenessError()` | `0xc35cc440` | Generic idleness-module error | — | +| `InsufficientActiveValidators(uint256,uint256)` | `0xb5e5b936` | Not enough active validators for the request (numValidators, availableValidators) | Wait for more active validators | +| `InvalidAppealBond()` | `0xc59a6168` | Appeal bond value is invalid | Provide a valid bond | +| `InvalidAppealRounds()` | `0x2b4f0027` | Configured appeal-rounds value is invalid | Set a valid rounds value | +| `InvalidCommitHash()` | `0x173d238e` | Commit hash is invalid | Provide a valid commit hash | +| `InvalidCommittedValidators()` | `0xcbf18bce` | Committed-validators set is invalid | Correct committed validators | +| `InvalidDeploymentWithSalt()` | `0xaceab4a1` | CREATE2 deployment-with-salt is invalid | Fix salt/init params | +| `InvalidGhostContract()` | `0x1d41354d` | Ghost contract is invalid | Target a valid ghost contract | +| `InvalidIdleReplacementIndex(address,uint256,uint256)` | `0x4424217a` | Idle-replacement index mismatch (validator, expected, provided) | Provide the expected index | +| `InvalidNumOfValidators()` | `0xc4be27c5` | Validator count is invalid | Provide a valid count | +| `InvalidPhaseTimeoutBounds()` | `0x7cee0061` | Phase timeout bounds are invalid | Fix min/max bounds | +| `InvalidProcessingBlock()` | `0xd1ba0787` | Processing block is invalid | Use a valid processing block | +| `InvalidRevealData()` | `0xbc03c4b4` | Reveal payload is invalid | Provide valid reveal data | +| `InvalidRevealLeaderData()` | `0x92c313ee` | Leader reveal payload is invalid | Provide valid leader reveal data | +| `InvalidSender()` | `0xddb5de5e` | Sender is invalid | Provide a valid sender | +| `InvalidTimestampType()` | `0x099d113d` | Timestamp type is invalid | Provide a valid timestamp type | +| `InvalidTransactionStatus()` | `0xf8062102` | Tx status is invalid for this operation | Act only in the required status | +| `InvalidTribunalAppealStatus()` | `0x37c1c7d4` | Tribunal appeal status invalid for this action | Act only in the required status | +| `InvalidTxExecutionHash()` | `0x22a529c0` | Tx execution hash is invalid | Provide the correct execution hash | +| `InvalidValidator()` | `0x682a6e7c` | Validator is invalid | Provide a valid validator | +| `InvalidValidatorsLength()` | `0x5d67a037` | Validators array length is invalid | Provide correct array length | +| `InvalidVote()` | `0xd5dd0c66` | Vote value is invalid | Provide a valid vote | +| `InvalidVoteType()` | `0x8eed55d1` | Vote type is invalid | Provide a valid vote type | +| `LeaderResultHashAlreadySet()` | `0x584764ae` | Leader result hash has already been set | Set the leader result once | +| `MaxNumOfIterationsInPendingQueueReached()` | `0x357bf18b` | Pending-queue iteration cap reached | Retry/continue processing later | +| `MaxNumOfMessagesExceeded(uint256,uint256)` | `0x3838b192` | Message count exceeds allocation (numOfMessages, maxAllocatedMessages) | Emit fewer messages | +| `MockZkSyncBridgeCallFailedToL2()` | `0x9a245636` | Mock zkSync L2 bridge call failed (test) | Test/mock harness failure | +| `NoIdleValidator()` | `0xede1b7ce` | No idle validator is available | Wait for an idle validator | +| `NonGenVMContract()` | `0xc1ba7c94` | Target is not a GenVM (ghost) contract | Target a GenVM contract | +| `NoPendingRefund()` | `0xfb093898` | No pending refund to process | Nothing to refund | +| `NoRotationsLeft()` | `0xe0bf2581` | No leader rotations remaining | Rotation budget exhausted | +| `NoSenderForTransaction()` | `0xa0b18673` | Transaction has no sender | Provide a valid sender | +| `NotConsensus()` | `0x70f64de5` | Caller is not the consensus contract | Only consensus may call | +| `NotConsensusOrIdleness()` | `0xa82180be` | Caller is not consensus or idleness module | Restricted to consensus/idleness | +| `NotConsensusOrIdlenessOrTransactions()` | `0x1e3f968f` | Caller is not consensus, idleness, or transactions module | Restricted to those three modules | +| `NotConsensusOrTransactions()` | `0xe6533a27` | Caller is not consensus or transactions module | Restricted to consensus/transactions | +| `NotGenConsensus()` | `0xfd9abcdc` | Caller is not GenConsensus | Only GenConsensus may call | +| `NotIdleness()` | `0x878f6816` | Caller is not the idleness module | Only idleness may call | +| `NoValidatorsFound()` | `0x9b7fa1e5` | No validators were found | Ensure validators exist | +| `NumOfMessagesIssuedTooHigh()` | `0x5013bc2a` | Number of issued messages is too high | Reduce issued messages | +| `OutOfGas()` | `0x77ebef4d` | Out-of-gas surfaced as a typed revert | Increase gas limit | +| `PendingQueueFull(address,uint256)` | `0xd48a82a3` | Recipient's pending message queue is full (recipient, max) | Drain queue or wait | +| `PhaseTimeoutOutOfBounds(uint256,uint256,uint256)` | `0xdb0c8dfe` | Phase timeout is outside allowed bounds (value, minBound, maxBound) | Use a timeout within bounds | +| `QueueHeadExceedsTail()` | `0x698f39ad` | Queue head index exceeds tail (corruption guard) | Internal queue invariant broke | +| `RandomSeedAlreadySet()` | `0x7d6b9724` | Random seed has already been set | Set the seed once only | +| `ReconcileBeyondTail()` | `0x34cf41dd` | Reconcile index is past the queue tail | Reconcile within valid range | +| `ReconcileNotAdvancing()` | `0x3a1a617c` | Reconcile made no progress | Nothing to reconcile | +| `RecoverRangeBeyondIssued()` | `0x82eacf35` | recoverRecipient range is past the issued count | Use a range within issued | +| `RecoverRangeInvalid()` | `0xd1402d7e` | recoverRecipient range is invalid | Provide a valid range | +| `RemovingTransactionFromPendingQueueFailed()` | `0x866b818f` | Failed to remove tx from the pending queue | Internal queue operation failed | +| `TransactionAlreadyAccepted()` | `0x7bdaa2b4` | Transaction has already been accepted | No re-acceptance | +| `TransactionCanNotBeAddedToAcceptedQueue()` | `0xcdcfa366` | Tx cannot be added to the accepted queue | State disallows enqueue | +| `TransactionCanNotBeAddedToPendingQueue()` | `0x406a3bbb` | Tx cannot be added to the pending queue | State disallows enqueue | +| `TransactionCanNotBeAddedToUndeterminedQueue()` | `0x69842b0a` | Tx cannot be added to the undetermined queue | State disallows enqueue | +| `TransactionCanNotBeFinalized()` | `0xd9be37ca` | Transaction is not eligible for finalization | Not in a finalizable state | +| `TransactionInRecomputation()` | `0x00ebaa7c` | Transaction is currently under recomputation | Wait for recomputation to finish | +| `TransactionNotAcceptedNorUndetermined()` | `0x90cb8b61` | Transaction is neither accepted nor undetermined | Only valid in those states | +| `TransactionNotAtAcceptedQueueHead()` | `0x3e714edf` | Tx is not at the head of the accepted queue | Process the head tx first | +| `TransactionNotAtPendingQueueHead()` | `0x0844056a` | Tx is not at the head of the pending queue | Process the head tx first | +| `TransactionNotAtUndeterminedQueueHead()` | `0x3d40531f` | Tx is not at the head of the undetermined queue | Process the head tx first | +| `TransactionNotFinalized()` | `0xe4e81f79` | Transaction is not finalized | Finalize the tx first | +| `TransactionNotFound()` | `0x31fb878f` | Transaction not found | Reference a valid tx | +| `TransactionNotInPendingQueue()` | `0x7b9ea34f` | Tx is not in the pending queue | Only valid for queued txs | +| `TransactionNotTerminal()` | `0x1d3a409a` | Transaction is not in a terminal state | Only valid for terminal txs | +| `TransactionStillValid()` | `0xc4afd8fa` | Transaction is still valid (cannot treat as expired) | Wait until it is no longer valid | +| `TribunalAlreadyFinalized()` | `0x6cb27c0f` | Tribunal has already been finalized | No re-finalization | +| `TribunalNotFound()` | `0x609bbfa8` | Referenced tribunal does not exist | Reference a valid tribunal | +| `UnfinishedTransactions()` | `0xf082b82d` | Unfinished transactions block the operation | Finish pending txs first | +| `UnfinishedTxCounterUnderflow()` | `0x8894ba94` | Unfinished-transaction counter underflowed | Internal accounting invariant broke | +| `ValidatorAlreadyCommitted()` | `0xf8961aee` | Validator has already committed | Commit once per round | +| `ValidatorAlreadyRevealed()` | `0x00d6a91b` | Validator has already revealed | Reveal once per round | +| `ValidatorAlreadyVoted()` | `0x6e271ebe` | Validator has already voted | Vote once per round | +| `ValidatorSelectionFailed()` | `0x1f90236d` | Validator selection algorithm failed | Check validator set/seed | +| `ValidatorWalletAlreadyDeployed()` | `0xb9ceb484` | Validator wallet already deployed | Do not redeploy the wallet | +| `ValidValidatorNotFound()` | `0x1c177f6c` | No valid validator was found | Ensure eligible validators exist | +| `VoteAlreadyCommitted()` | `0xcf1c5b9c` | Vote has already been committed | Commit vote once | +| `VoteAlreadyRevealed()` | `0x3246ac36` | Vote has already been revealed | Reveal vote once | +| `WalletDeploymentFailed()` | `0x6e09c9eb` | Validator wallet deployment failed | Check factory/CREATE2 params | +| `WrongRecomputationTransaction()` | `0x4d1fe80e` | Recomputation targeted the wrong transaction | Target the correct tx | + +### Governance (9) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `CallerNotGovernance()` | `0xf2be30fb` | Caller is not the governance contract | Route via governance | +| `GovernanceInsufficientValue(uint256,uint256)` | `0xf498db0c` | msg.value insufficient for the operation (provided, required) | Send the required value | +| `GovernanceInvalidDelay()` | `0xd1132ebc` | Configured timelock delay is invalid | Set a valid delay | +| `GovernanceOperationExpired(address,bytes4,bytes,uint256,uint256)` | `0x11515806` | Queued operation expired past its window (target, selector, args, value, expiry) | Re-queue the operation | +| `GovernanceOperationNotFound(address,bytes4,bytes,uint256)` | `0x8ff7bbe1` | Operation not found in the queue (target, selector, args, value) | Queue it first | +| `GovernanceOperationPending(address,bytes4,bytes,uint256)` | `0xc7eef27f` | Operation is already queued/pending (target, selector, args, value) | Do not re-queue | +| `GovernanceTargetIsNotAContract(address)` | `0x83e02672` | Governance target address has no code (target) | Target a deployed contract | +| `GovernanceTimelockPending(address,bytes4,bytes,uint256,uint256)` | `0x2189e680` | Timelock not yet elapsed (target, selector, args, value, eta) | Wait until ETA | +| `ZeroValue()` | `0x7c946ed7` | Value or parameter is zero | Provide a non-zero value | + +### Access (7) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `CallerNotAuthorized()` | `0xc183bcef` | Caller is not authorized for this action | Use an authorized address | +| `CallerNotOwner()` | `0x5cd83192` | Caller is not the owner | Call from the owner | +| `NotAppealsContract()` | `0xa5a4297b` | Caller is not the appeals contract | Only the appeals contract may call | +| `NotOperator()` | `0x7c214f04` | Caller is not an operator | Call from an operator address | +| `NotStaking()` | `0x890fec52` | Caller is not the staking contract | Only staking may call | +| `Unauthorized()` | `0x82b42900` | Generic unauthorized caller | Use an authorized caller | +| `ZeroAddress(string)` | `0xeac0d389` | A named address is the zero address (key) | Provide a non-zero address | + +### Other (11) + +| Error | Selector | Meaning | Common cause / fix | +|:--|:--|:--|:--| +| `ArrayLengthMismatch()` | `0xa24a13a6` | Two input arrays have different lengths | Match array lengths | +| `BatchSizeExceeded(uint256,uint256)` | `0xf80a4845` | Batch size exceeds the allowed maximum (provided, maximum) | Reduce batch size | +| `IndexOutOfBounds()` | `0x4e23d035` | Array index is out of bounds | Use a valid index | +| `InvalidAddress()` | `0xe6c4247b` | Address parameter is invalid or zero | Provide a valid address | +| `InvalidNonce()` | `0x756688fe` | Nonce is invalid or out of order | Use the correct nonce | +| `InvalidNumber(uint256)` | `0xc5d83cde` | Generic numeric-validation failure (number) | Provide a value in range | +| `InvalidOffset()` | `0x01da1572` | Pagination offset is invalid | Use a valid offset | +| `InvalidPageSize()` | `0xe5b7db2e` | Pagination page size is invalid | Use a valid page size | +| `InvalidVersion()` | `0xa9146eeb` | Version mismatch | Match the expected version | +| `PercentageOutOfRange(uint256)` | `0xafd5d0b0` | Percentage value is out of the allowed range (value) | Provide a valid percentage | +| `ZeroTotalWeight()` | `0x098404de` | Total weight is zero (test mock) | Ensure non-zero total weight | diff --git a/pages/developers/staking-guide.mdx b/pages/developers/staking-guide.mdx index df228bdc..7a01d4cb 100644 --- a/pages/developers/staking-guide.mdx +++ b/pages/developers/staking-guide.mdx @@ -469,4 +469,5 @@ genStaking.validatorClaim(validatorWallet); - [Staking Concepts](/understand-genlayer-protocol/core-concepts/optimistic-democracy/staking) - How staking works in GenLayer - [Unstaking](/understand-genlayer-protocol/core-concepts/optimistic-democracy/unstaking) - Unstaking process details - [Slashing](/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing) - Slashing penalties and conditions +- [Error & Revert Reference](/developers/error-reference) - Look up a custom error selector returned by a staking call - [GenLayer CLI Staking Commands](/api-references/genlayer-cli#staking-operations-testnet) - CLI reference for staking diff --git a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/_meta.json b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/_meta.json index 8c046227..6707e9e8 100644 --- a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/_meta.json +++ b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/_meta.json @@ -1,6 +1,8 @@ { "equivalence-principle": "", "appeal-process": "", + "deterministic-violations-and-tribunals": "", + "protocol-randomness": "", "finality": "", "staking": "", "slashing": "", diff --git a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process.mdx b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process.mdx index 45791218..b431f0be 100644 --- a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process.mdx +++ b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process.mdx @@ -19,3 +19,7 @@ Once a consensus is reached, the final decision is recorded, and the transaction ## Gas Costs for Appeals The gas costs for an appeal can be covered by the original user, the appellant, or any third party. When submitting a transaction, users can include an optional tip to cover potential appeal costs. If insufficient gas is provided, the appeal may fail to be processed, but any party can supply additional gas to ensure the appeal proceeds. + +## Related + +The appeals process handles *disputed* outcomes. A separate, automatic mechanism — the **tribunal** — handles the case where a validator produced a provably-wrong *deterministic* result. See [Deterministic Violations & Tribunals](/understand-genlayer-protocol/core-concepts/optimistic-democracy/deterministic-violations-and-tribunals) for how those are detected and adjudicated. diff --git a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/deterministic-violations-and-tribunals.mdx b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/deterministic-violations-and-tribunals.mdx new file mode 100644 index 00000000..4401318e --- /dev/null +++ b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/deterministic-violations-and-tribunals.mdx @@ -0,0 +1,107 @@ +--- +description: "Deterministic Violations and Tribunals explains how GenLayer detects a validator that produced a provably-wrong deterministic result, how that differs from idleness, and how a tribunal adjudicates the dispute and applies slashing." +--- + +import { Callout } from 'nextra-theme-docs' + +# Deterministic Violations & Tribunals + +GenLayer transactions mix two kinds of work. **Non-deterministic** operations — calling a language model, reading the web — can legitimately differ between validators and are reconciled through the [Equivalence Principle](/understand-genlayer-protocol/core-concepts/optimistic-democracy/equivalence-principle) and [appeals](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process). **Deterministic** operations — ordinary contract computation — must produce the *same* result for everyone who runs them. When a validator reports a deterministic result that disagrees with everyone else's, that is a different and more serious matter: a **deterministic violation**. + +This page explains what a deterministic violation is, how the protocol tells it apart from a validator that simply went idle, how a **tribunal** adjudicates the dispute, and what the consequences are. + +## What Is a Deterministic Violation? + +A deterministic violation is a validator (often the round leader) reporting a deterministic execution result that is provably inconsistent with the result the rest of the committee computed. + +Because deterministic execution is reproducible, every validator that runs a transaction should arrive at the same result, and therefore the same result hash. During the normal commit-and-reveal flow, validators reveal a hash of their execution result. The protocol compares the leader's reported execution hash against each validator's revealed result hash: + +- **Hashes match** — the validator agrees with the leader; this counts toward the majority-agree tally. +- **Hashes disagree** — the mismatch is recorded as a deterministic violation. + +If a majority of the committee disagrees with the leader's deterministic result, the round's outcome is classified as a deterministic violation and the dispute is escalated to a tribunal. + + +"Provably wrong" here means *inconsistent with the on-chain majority of validators' result hashes* — not the product of an on-chain re-execution. The consensus contracts compare hashes that validators computed off-chain and committed; they do not re-run the program themselves. The guarantee comes from agreement among independently-executing validators. + + +## Violations vs. Idleness + +The protocol draws a sharp line between two categories of fault, and handles them on separate paths with different consequences: + +| | **Idleness** (liveness fault) | **Deterministic violation** (safety fault) | +|:--|:--|:--| +| **What happened** | A validator failed to act within its phase timeout | A validator actively reported a wrong deterministic result | +| **Nature** | Missing participation | Provably incorrect participation | +| **Detected by** | The idleness module tracking who acted in time | Result-hash comparison during voting | +| **Immediate consequence** | A strike is recorded; a per-strike slash applies | The result is escalated to a tribunal; the accused leader is quarantined for the tribunal's duration | +| **Escalated outcome** | Temporary quarantine once strikes cross a threshold | Slashing of the parties the tribunal finds at fault | + +The distinction matters: idleness is about *availability*, and its penalties are designed to be recoverable — a validator that misses windows is temporarily sidelined and can return. A deterministic violation is about *correctness*, and it is adjudicated more heavily because a validator that reports wrong deterministic results undermines the integrity of the chain. + +### How Idleness Is Handled + +When a validator misses a required action within a phase, the idleness module records a **strike** for that validator in the current epoch, and applies a slash for that strike. Strikes accumulate over the epoch; when a validator's strike count crosses the configured maximum, the validator is placed into a **temporary quarantine** and excluded from selection for a bounded number of epochs, after which it can be reinstated. The strike threshold is a governance parameter (set via `setStrikesMax`), so the exact number of missed actions tolerated per epoch is configurable rather than fixed by the protocol. + +This is the ordinary, self-correcting side of validator discipline. For the full penalty mechanics, see [Slashing](/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing). + +## The Tribunal Process + +A tribunal is the mechanism that adjudicates a suspected deterministic violation. Unlike a user-initiated appeal, a tribunal is triggered **automatically by the protocol** when a consensus round's majority result is a deterministic violation. + +The process runs roughly as follows: + +1. **Trigger.** When a round resolves to a deterministic violation, the protocol prepares a tribunal for that transaction and immediately places the accused leader under a temporary quarantine for the duration of the adjudication. + +2. **Assembly.** The tribunal considers the votes already cast in the original round and opens participation to a broader set of eligible validators, so that more of the validator set weighs in on whether the leader's deterministic result was actually wrong. + +3. **Commit and reveal.** Participating validators adjudicate by committing and then revealing a vote over the execution result — the same commit-reveal discipline used elsewhere in consensus, which prevents validators from copying one another. Each revealed vote is reduced to a result hash and compared against the leader's result hash to classify the voter as agreeing or disagreeing with the leader. + +4. **Verdict.** Once the reveal window closes, the tribunal is finalized into one of three outcomes: + - **Majority disagree** — the violation is *upheld*: the committee confirms the leader's deterministic result was wrong. + - **Majority agree** — the leader is *vindicated*: the accusation does not hold, and the leader's temporary quarantine is lifted. + - **No majority** — the tribunal is inconclusive, the quarantine is lifted, and no slashing is applied. + + +The precise rule for what constitutes a tribunal "majority" — in particular how non-voting validators are counted — is an area where the current implementation and the design intent diverge. Treat the outcomes above as the shape of the mechanism rather than an exact quorum specification, and consult the protocol's design records for the authoritative rule. + + +## Consequences + +The consequences of a tribunal fall into two parts: **slashing**, which is implemented and applied today, and a **permanent ban**, which is the designed end-state for a proven violation. + +### Slashing + +When a violation is upheld (majority disagree), the protocol slashes the parties at fault: + +- The **leader** who reported the wrong deterministic result is slashed at the leader rate. +- **Validators who sided with the wrong result** (or who failed to weigh in) are slashed at the validator rate. + +Conversely, if the leader is vindicated (majority agree), the validators who *wrongly accused* the leader are the ones slashed, and the leader is left untouched. This symmetry discourages both dishonest leaders and frivolous accusations. + +The slash amounts are expressed as percentages of stake and are governance-configurable, with a cap on how much a validator can be slashed in a single epoch. As currently configured, a leader's deterministic-violation slash is several times larger than a validator's, reflecting the leader's greater responsibility for the reported result. Slashes do not take effect instantly: they are scheduled and only become final after a short delay measured in epochs, which leaves room for the outcome to settle before stake is actually removed. + +### Permanent Ban (Designed Consequence) + +By design, a validator whose deterministic violation is upheld is intended to be **permanently removed** from the validator set — a permanent quarantine, in contrast to the temporary, recoverable quarantine used for idleness. This reflects the principle that producing provably-wrong deterministic results is a safety fault that should not be forgiven the way a missed window is. + + +Permanent banning of a leader on an upheld deterministic violation is the **intended** consequence described in the protocol's design records, and the underlying banning mechanism exists in the staking contracts. In the current consensus code it is **not yet wired into the tribunal outcome**: an upheld violation applies slashing and leaves the accused leader's temporary quarantine in place (it is simply not lifted), but the permanent-ban call is not invoked automatically. Describe permanent removal as the design target, and verify the live behavior against the deployed contracts before relying on it. + + +## Relationship to Appeals + +GenLayer has two distinct escalation mechanisms, and it is worth keeping them separate: + +- The **[appeals process](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process)** is a permissionless, bond-backed way for anyone to challenge a transaction's outcome and pull in additional validators to reassess a *disputed* decision (for example, a contested non-deterministic result or a timeout). +- A **tribunal** is an automatic, protocol-initiated adjudication that fires specifically when a round's majority result is a *deterministic violation*. No user bond initiates it; it is a built-in response to the violation itself, and its job is to confirm or overturn the violation finding and apply the resulting slashing. + +Both draw on a wider pool of validators to increase confidence in the result, but they are triggered differently and serve different purposes. + +## Related Concepts + +- [Appeals Process](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process) — the bond-backed challenge mechanism for disputed outcomes. +- [Slashing](/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing) — how stake penalties are calculated and finalized. +- [Staking Penalties](/understand-genlayer-protocol/core-concepts/optimistic-democracy/staking-penalties) — the broader catalog of penalties applied to staked validators. +- [Protocol Randomness](/understand-genlayer-protocol/core-concepts/optimistic-democracy/protocol-randomness) — how the validators and leaders in these committees are selected. +- [Equivalence Principle](/understand-genlayer-protocol/core-concepts/optimistic-democracy/equivalence-principle) — how legitimate non-deterministic differences are reconciled. diff --git a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/protocol-randomness.mdx b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/protocol-randomness.mdx new file mode 100644 index 00000000..62efc500 --- /dev/null +++ b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/protocol-randomness.mdx @@ -0,0 +1,64 @@ +--- +description: "Protocol Randomness explains how GenLayer derives the seed used for validator and leader selection, how the seed advances each round, and the unpredictability and anti-grinding properties of the mechanism." +--- + +import { Callout } from 'nextra-theme-docs' + +# Protocol Randomness + +Every transaction in GenLayer is validated by a committee of validators, one of whom acts as the leader. To keep the network fair and censorship-resistant, the protocol must decide *which* validators serve on each committee in a way that no single operator can predict far in advance or bias in their own favor. That decision is driven by an on-chain source of randomness that is derived fresh for every round of consensus. + +This page describes, conceptually, how that randomness is produced, how it is consumed for selection, and what unpredictability guarantees it does — and does not — provide. + +## The Randomness Mechanism + +GenLayer's randomness is **seed-chained**: the protocol maintains a numeric *seed*, and each round advances that seed to a new value that feeds the next selection. The advance is driven by a participant's signature over the current seed. + +Concretely, the mechanism combines two standard primitives: + +- **An ECDSA signature over the current seed.** To advance the seed, a participant signs the current seed value with their validator key. The protocol recovers the signer's address from the signature and requires it to match the expected participant, so only the designated participant can advance the seed for their step. + +- **A keccak256 hash to produce the next seed.** The new seed is the keccak256 hash of the signature combined with the previous seed. Because the signature is unpredictable until it is produced, and hashing is one-way, the resulting seed is effectively unpredictable to anyone who does not yet hold that signature. + + +This is a signature-driven, hash-chained randomness scheme — **not** a verifiable random function. The "proof" that advances the seed is an ordinary ECDSA signature over the seed, and the next seed is simply a keccak256 hash. It provides freshness and replay-resistance, but it does not carry the formal, independently verifiable unbiasability guarantees of a dedicated cryptographic random-function construction. The known limitations are described in [Known Limitations](#known-limitations) below. + + +### From Seed to Selection + +Selecting a committee member from a set of candidates is a separate step that consumes seeds. The protocol derives an index into the candidate set by hashing a combination of seeds together and reducing the result over the size of the set. Repeating this with the advancing seed yields the successive members of a committee. + +Selection is **stake-weighted**: a validator's chance of being chosen scales with the stake it has at risk, rather than every validator being equally likely. Higher-staked validators are therefore selected more often, which ties influence over consensus to economic commitment. The randomness supplies the unpredictable input; the stake weighting supplies the distribution. + +### Per-Recipient Chains and Per-Round Freshness + +The evolving seed is maintained **per recipient** — that is, per target contract address — rather than as a single global value. Each time a transaction to that recipient is activated or proposed, the recipient's seed advances. Because it advances every time it is used, each selection draws on a different seed than the last: a committee assembled for one transaction does not reuse the randomness of a previous one, and the seed that fixes a given transaction's committee is captured at activation time. This rotation is what makes committee membership a moving target rather than a fixed schedule an operator could plan around. + +## Why Selection Cannot Be Gamed + +The design goal is that an operator cannot arrange to be selected (or to avoid selection) for a particular transaction. Several properties work together toward that goal: + +- **The seed depends on inputs the operator does not fully control.** Advancing the seed requires a signature from a specific participant, and the resulting value is hashed. An operator cannot freely choose the next seed; they can only contribute their prescribed signature at their prescribed step. + +- **The seed is consumed as it advances.** Selection and seed advancement are bound together in the consensus flow: the seed used for a transaction's committee is fixed at activation and read from there, so it cannot be re-derived mid-transaction to fabricate a favorable outcome. Once the seed advances, a signature that was valid against the old seed no longer verifies against the new one, so past proofs cannot be replayed to roll the seed back. + +- **Selection is fresh per round.** Even a participant who learns the current seed only learns it for the round at hand; the next round's seed depends on a signature that does not yet exist. + +Together these mean that, in the normal case, a validator learns whether it was selected at the moment selection happens — not early enough to reposition stake or otherwise engineer the outcome. + +## Known Limitations + +GenLayer documents the properties of this mechanism plainly, including where it is weaker than an idealized random beacon: + +- **Grinding at the margins.** Because a participant produces the signature that advances the seed, a participant who is willing to withhold or choose *when* to submit has some limited ability to influence the resulting value — a "grinding" surface. The scheme constrains this (the signer is fixed and the output is hashed), but it does not offer the formal grinding-resistance of a construction whose output no single party can compute alone. + +- **Initial-seed predictability.** Each recipient's seed chain has to start somewhere, and it is derived in part from block data (block hash, timestamp, number) that a block producer has bounded influence over. In principle this means the *first-ever* selection for a brand-new recipient could be weakly predicted by an actor able to influence block production. The impact is limited and self-correcting: only that first selection is affected, and every subsequent signature-driven advance restores full unpredictability. + +These are acknowledged characteristics of the current design rather than latent bugs. They are called out here so that integrators reason about validator selection with an accurate model of its guarantees. + +## Related Concepts + +- [Validators and Validator Roles](/understand-genlayer-protocol/core-concepts/validators-and-validator-roles) — the roles (leader, validator) that selection assigns. +- [Optimistic Democracy](/understand-genlayer-protocol/core-concepts/optimistic-democracy) — how selected committees reach consensus. +- [Appeals Process](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process) — how additional validators are drawn when a decision is challenged. +- [Staking](/understand-genlayer-protocol/core-concepts/optimistic-democracy/staking) — the stake that weights selection. diff --git a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing.mdx b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing.mdx index 00737911..f1547d49 100644 --- a/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing.mdx +++ b/pages/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing.mdx @@ -27,3 +27,7 @@ Validators in GenLayer can be slashed for several reasons: ### Amount Slashed The amount slashed varies based on the severity of the violation and the specific rules set by the GenLayer platform. The slashing amount is designed to be substantial enough to deter malicious or negligent behavior while not being excessively punitive for honest mistakes. + +## Related + +The most serious slashable fault — a validator reporting a provably-wrong deterministic result — is adjudicated by a tribunal before stake is removed. See [Deterministic Violations & Tribunals](/understand-genlayer-protocol/core-concepts/optimistic-democracy/deterministic-violations-and-tribunals) for how a violation is detected, how it differs from idleness, and how the resulting slash is applied. diff --git a/pages/validators/_meta.json b/pages/validators/_meta.json index d1fda8ca..511b9a1c 100644 --- a/pages/validators/_meta.json +++ b/pages/validators/_meta.json @@ -4,5 +4,6 @@ "system-requirements": "System Requirements", "genvm-configuration": "GenVM Configuration", "upgrade": "Upgrade Guide", + "network-keeper-roles": "Network Keeper Roles", "changelog": "Changelog" } diff --git a/pages/validators/network-keeper-roles.mdx b/pages/validators/network-keeper-roles.mdx new file mode 100644 index 00000000..5fda9afc --- /dev/null +++ b/pages/validators/network-keeper-roles.mdx @@ -0,0 +1,208 @@ +--- +description: "Network keeper roles are the permissionless GenLayer functions anyone can call to keep the network live — inflation relay, epoch advancement, validator priming, appeals, finalization, and cleanup — with their triggers, rewards, and risks." +--- +import { Callout } from "nextra-theme-docs"; + +# Network Keeper Roles + +A **keeper role** is a permissionless smart-contract function that the GenLayer network needs someone to call in order to keep running — advancing epochs, relaying inflation across the L1↔L2 bridge, priming validators, finalizing transactions, and cleaning up expired state. None of these functions require special privileges: **anyone can call them**, from a validator operator to an independent community bot. + +Today the GenLayer Foundation operates a baseline set of these keepers so the network stays live without any community involvement. But because the functions are permissionless, community keepers can run them too — either as a backstop when Foundation services lag, or to earn the rewards attached to some roles. Running keepers is a way to contribute to network liveness and, where a reward exists, to be paid for it. + + + Most keeper work is already automated. Validator nodes finalize transactions and prime validators as part of normal operation, and epoch advancement plus token burning fire automatically inside `epochAdvance()`. The roles below matter most as a **liveness backstop**: they document what can be triggered manually if the automated path stalls, and which triggers carry a reward. + + +## Role Summary + +| Role | Function | Who may call | Reward | Risk if idle | +|---|---|---|---|---| +| **Inflation relayer** | `GEN.executeL2Message()` (L1) | Anyone (permissionless) | `incentivePercentage` × epoch inflation, minted to caller. | **Epoch progression halts network-wide.** L2 cannot advance past `inflationEpoch`. | +| **Epoch advancer** | `Staking.epochAdvance()` (L2) | Anyone (permissionless) | None | Epochs stop advancing; fees/inflation are not distributed. | +| **Validator primer** | `Staking.validatorPrime(validator)` (L2) | Anyone, on behalf of a validator | 1% of any slash that fires (only when a slash is pending and caller ≠ validator). Healthy validator → none. | Validator misses selection; pending slashes go unenforced. | +| **Appellant** | `ConsensusMain.submitAppeal()` | Anyone (permissionless, payable) | Successful appeal pays 1.5× bond (1× principal returned + 0.5× profit). | Incorrect results are not challenged. | +| **Finalizer** | `finalizeTransaction()` | Anyone (permissionless) | None | Transactions do not settle; fees and appeal payouts are not released. | +| **Stuck-tx healer** | `advanceStuckTransaction(txId)` | Anyone (permissionless) | None | A transaction whose stored status lags its computed status stays stuck. | +| **Burn trigger** | `StakingInflation.burn()` (L2) | Anyone (permissionless) | None | Burnable GEN accumulates instead of leaving supply (only if below the auto-burn threshold). | +| **Quarantine GC** | `StakingBan.cleanupExpiredQuarantines()` (L2) | Anyone (permissionless) | None | Expired quarantine records are not reclaimed (storage bloat only). | + + + **Scope.** This page covers only permissionless keeper functions. Governance-gated actions — unbanning validators (`QUARANTINE_MANAGER_ROLE`), admin/emergency ("red-button") surfaces — are **not** keeper roles and are restricted to authorized operators. + + +--- + +## Inflation Relayer + +**Function:** `GEN.executeL2Message(batchNumber, txIndex, batchIndex, message, proof, l2GasPrice, l2GasLimit)` on the **L1** GEN token contract. + +Each epoch's inflation makes a round trip: the L2 Staking contract requests it, L1 mints and bridges it back, and L2 receives it. The middle step — proving the L2→L1 message on L1 and relaying it — **cannot happen on-chain**, because zkSync L2→L1 messages can only be proved after the L2 batch has finalized on L1 (roughly an hour on real networks). Someone has to call `executeL2Message()` off-chain to complete the round trip. + + + **This role gates the entire network's epoch progression.** The L2 `Staking.epochAdvance()` can only move forward while `epoch + 1 <= inflationEpoch`, and `inflationEpoch` only rises when `executeL2Message()` is successfully relayed on L1. **If no one relays inflation, epoch progression halts network-wide** once the current epoch catches up to `inflationEpoch`. This is the single most important keeper to keep running. + + +**When to call:** whenever an inflation request message is pending relay. Pre-check any candidate message — and estimate the reward — with the L1 view helper: + +```solidity +// Returns (pending, rewards): whether the message still needs relaying, +// and the estimated GEN reward for relaying it. +(bool pending, uint256 rewards) = GEN.isL2MessagePending( + batchNumber, txIndex, batchIndex, message +); +``` + +**Reward:** on a successful INFLATION relay, the caller is paid + +``` +reward = totalInflation × incentivePercentage / 10000 +``` + +minted to `msg.sender` on L1, emitting `IncentiveRewardPaid(caller, reward)`. The reward is **carved out of** that epoch's inflation, not minted on top — total new supply is `fees + totalInflation` regardless of the incentive. + +**Cadence:** relay each pending inflation message shortly after its L2 batch finalizes on L1 (about hourly on testnet, gated by batch finalization). Relaying is idempotent per target epoch — a duplicate request for an already-requested epoch is a safe no-op — so a keeper that occasionally double-submits does no harm. + +--- + +## Epoch Advancer + +**Function:** `Staking.epochAdvance()` on the L2 Staking contract. + +Advances the network from the current epoch to the next. On each advance it distributes the finalized epoch's fees and inflation to validators, developers, and the DAO, requests the next window of inflation from L1 (see [Inflation Relayer](#inflation-relayer)), and — when the burn threshold is met — auto-triggers [token burning](#burn-trigger). + +**When to call:** once the current epoch's minimum duration has elapsed and its transactions are resolved. In normal operation this is driven automatically; a keeper only needs to step in if advancement stalls. + +**Reward:** none. `epochAdvance()` pays no caller incentive. + +**Cadence:** at most once per epoch boundary. Advancement will not run ahead of the inflation bridge — see the halts-chain warning above — so a stalled `inflationEpoch` is diagnosed at the inflation relayer, not here. + +--- + +## Validator Primer + +**Function:** `Staking.validatorPrime(validator)` on the L2 Staking contract, callable by anyone on behalf of any validator. + +Priming rolls a validator's stake forward for the upcoming epoch — committing staged deposits and withdrawals, distributing rewards, and **lazily enforcing any pending slash** against that validator. A validator's own node primes it automatically each epoch; the permissionless surface exists so that a validator with a pending slash cannot escape enforcement simply by not priming itself. + +**When to call:** to enforce a slash that a validator is avoiding, or as a backstop if a validator's node has stopped priming. Priming is also what activates deposits (see the [Staking Contract Guide](/developers/staking-guide#depositing-as-a-validator)). + +**Reward:** when priming applies a pending slash **and the caller is not the validator being primed**, the caller receives + +``` +callerReward = slashedAmount × 100 / 10000 // SLASH_CALLER_INCENTIVE_BPS = 100 = 1% +``` + +deducted from the epoch's slashed pool before the remainder is burned. Priming a **healthy** validator (no pending slash) pays **nothing** — do not expect a reward for routine priming. + +**Cadence:** validators prime themselves each epoch, so a keeper only needs to target validators that (a) have an enactable pending slash and are not priming, or (b) have stopped priming entirely. Related permissionless storage-cleanup helpers — `validatorClean(...)` and `delegatorClean(...)` — reclaim finalized deposit/withdrawal records and carry no reward. + + + Slashing is time-locked and lazy: a recorded penalty becomes enactable only 2 epochs after the event, and is applied at the next priming. A validator that **exits before being primed** can avoid a pending slash; the caller incentive exists partly to pay third parties to prime-and-slash before that happens. + + +--- + +## Appellant + +**Function:** `ConsensusMain.submitAppeal()` — permissionless and payable — on the consensus contract. + +Anyone may post a bond to appeal a transaction they believe was decided incorrectly, triggering re-evaluation by a fresh validator set. Unlike the other keeper roles, this one is **economically self-motivated**: a correct appeal is profitable, and a frivolous one forfeits the bond. + +**When to call:** when you have evidence that a finalized-pending transaction's result is wrong and are willing to stake a bond on it. Check the minimum bond first with the CLI: + +```bash +genlayer appeal-bond +genlayer appeal --bond # --bond auto-calculated if omitted +``` + +**Reward:** a **successful** appeal pays back **1.5× the bond** — the 1× principal returned plus a 0.5× profit — settled on finalization. A **failed** appeal forfeits 100% of the bond, which is paid out to the round's validators (not burned). See [Appeal Process](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process). + +**Cadence:** event-driven — appeal only when you have specific evidence of an incorrect result, never speculatively. + +--- + +## Finalizer + +**Function:** `finalizeTransaction()` on the consensus contract (`nonReentrant`). + +Settles a transaction once its acceptance window has passed and no successful appeal is outstanding: it releases fees, pays out any successful appellant, and marks the transaction final. + +**When to call:** after a transaction's acceptance window closes. This is normally driven by validator nodes as part of consensus, so a keeper only needs to step in if finalization is lagging. It can also be invoked from the CLI: + +```bash +genlayer finalize +``` + +**Reward:** none. Finalization pays no caller incentive; its purpose is liveness, not profit. + +**Cadence:** as needed to clear transactions whose windows have elapsed but which have not been finalized by validator nodes. + +--- + +## Stuck-Tx Healer + +**Function:** `advanceStuckTransaction(txId)` on the consensus contract's finalization phase — permissionless by design. + +Some transactions can end up with a **stored status that lags their computed status** — for example, a timeout has effectively occurred but the on-chain record has not caught up. `advanceStuckTransaction()` recomputes the current status and writes the catch-up, unblocking the transaction. It has no queue-head precondition (any stuck transaction can be healed, not just the head of the pending queue), is idempotent (a no-op on any transaction that is not in a pending family), and **does not itself finalize** — it is strictly a liveness improvement, not a privilege. + +**When to call:** when a specific transaction appears wedged — its stored state is behind where its timing says it should be. Safe to call speculatively because non-applicable transactions are no-ops. Emits `AdvanceStuckTransactionAttempted`. + +**Reward:** none. + +**Cadence:** on demand, in response to a stuck transaction. Because it is idempotent, a keeper can retry without side effects. + +--- + +## Burn Trigger + +**Function:** `StakingInflation.burn()` on the L2 Staking contract. + +Removes accumulated burnable GEN (unclaimed inflation, the post-incentive slashed pool, and unclaimable developer-inflation surplus) from supply by bridging it out of L2 and burning it on L1. + +**When to call:** almost never manually. Burning fires **automatically inside `epochAdvance()`** whenever the accumulated `burning` balance meets the configured `burnThreshold`. The manual call is a **fallback** for flushing balances that sit **below** the threshold, or when threshold automation is disabled (`burnThreshold = 0`, the default). + + + Completing a burn on L1 requires two further off-chain operator steps (`finalizeWithdrawal` then a BURN-payload `executeL2Message`) that pair with the L2 emission. Those steps do **not** pay the inflation relayer reward. See the staking system specification for the full burn flow. + + +**Reward:** none. + +**Cadence:** only when clearing sub-threshold balances; otherwise leave it to the automatic path. + +--- + +## Quarantine GC + +**Function:** `StakingBan.cleanupExpiredQuarantines(startIndex, maxIterations)` on the L2 Staking contract. + +Paginated garbage collection that reclaims **expired** quarantine records. It only removes quarantines that have already elapsed — it does **not** unban validators or alter any active quarantine (unbanning is a governance action, out of scope for keepers). + +**When to call:** periodically, to keep quarantine storage from bloating after many quarantine cycles. The `startIndex`/`maxIterations` pagination lets a keeper bound gas per call and sweep the set across several transactions. + +**Reward:** none — this is pure storage hygiene. + +**Cadence:** low-frequency housekeeping; there is no liveness deadline. + +--- + +## Running a Keeper + +**Network endpoints and contract addresses.** Keepers need the L2 Staking / consensus contract address and the L1 GEN token address for the target network. The L2 consensus `AddressManager` address and the chain RPC/WebSocket URLs for each testnet live in the [Network-Specific Consensus Configuration](/validators/setup-guide#network-specific-consensus-configuration) section of the setup guide. From the CLI, `genlayer network info` prints the active network's configuration and contract addresses. + +**Reference implementations.** The consensus repository ships reference scripts for the multi-step relay flows — the inflation relayer's proof-fetch-and-execute logic lives at `scripts/consensus_flows/staking/executeL2Message.ts`, alongside companion scripts for the other staking flows. Use these as a starting point for a production keeper: they show how to fetch the L2 receipt, poll `zks_getL2ToL1LogProof` until the batch is finalized, and submit the L1 call. + +**Practical guidance:** + +- **Prioritize the inflation relayer.** It is the only keeper whose absence halts the whole network, and the only one that pays a standing reward. Everything else is a backstop to already-automated behavior. +- **Check for a reward before assuming one.** Only the inflation relayer (via `incentivePercentage`), the validator primer (only on an actually-enforced slash), and the appellant (only on a successful appeal) ever pay. The rest are unpaid liveness/hygiene work. +- **Expect idempotency.** Inflation relays, stuck-tx healing, and quarantine GC are all safe to retry or double-submit — duplicate or non-applicable calls are no-ops, not errors. +- **Watch gas vs. reward.** For the paid roles, compare the estimated reward (e.g. `isL2MessagePending`'s `rewards`, or the pending slash amount) against the transaction cost before submitting. + +## Related Resources + +- [Setup Guide](/validators/setup-guide) — running a validator node and its network configuration +- [Staking Contract Guide](/developers/staking-guide) — direct Solidity interactions, `validatorPrime()`, and the epoch model +- [Staking Concepts](/understand-genlayer-protocol/core-concepts/optimistic-democracy/staking) — epochs, shares vs. stake, reward distribution +- [Slashing](/understand-genlayer-protocol/core-concepts/optimistic-democracy/slashing) — penalties, the 80/20 split, and lazy enforcement +- [Appeal Process](/understand-genlayer-protocol/core-concepts/optimistic-democracy/appeal-process) — how appeals re-evaluate a transaction +- [GenLayer CLI](/api-references/genlayer-cli) — `appeal`, `appeal-bond`, `finalize`, and `network info` commands diff --git a/pages/validators/setup-guide.mdx b/pages/validators/setup-guide.mdx index 7a82fb11..91b589f9 100644 --- a/pages/validators/setup-guide.mdx +++ b/pages/validators/setup-guide.mdx @@ -103,7 +103,7 @@ GenLayer validators use three distinct addresses: | Address | Description | Where Used | |---------|-------------|------------| | **Owner** | The only address that can withdraw staked funds. Keep this secure (cold wallet). | CLI wizard - signs staking transaction | -| **Operator** | Hot wallet on your server that signs blocks. Can be same as owner, but separate is recommended. | Node config: `operatorAddress` | +| **Operator** | Hot wallet on your server that signs blocks and submits validator transactions. It must hold liquid GEN to pay gas. Can be the same as the owner, but a separate account is recommended. | Node config: `operatorAddress` | | **Validator Wallet** | Smart contract created when you join. This is your validator's on-chain identity. | Node config: `validatorWalletAddress` | The wizard outputs all three addresses at the end. Save them - you'll need the Validator Wallet and Operator addresses for your node configuration. @@ -118,7 +118,7 @@ The wizard outputs all three addresses at the end. Save them - you'll need the V ```bash copy npm install -g genlayer ``` -- **GEN tokens** - You need at least **42,000 GEN** for the minimum self-stake requirement +- **GEN tokens** - You need at least **42,000 GEN** for the minimum self-stake requirement, plus a separate liquid balance for operator gas if you use a different operator account ### Using the Validator Wizard @@ -146,6 +146,28 @@ The wizard guides you through: The wizard creates and exports an operator keystore file for you to transfer to your validator server. If your server is compromised, your staked funds (controlled by owner) remain safe. +#### Fund the Operator Wallet + +The operator signs and submits on-chain validator duties, including epoch priming, proposals, votes, and finalization. A separate operator therefore needs a liquid GEN balance for gas. GEN staked through the Validator Wallet, including vesting-backed stake, cannot pay the operator's gas. + +Send a gas buffer to the operator address from a funded account before starting the node. You can use your wallet UI or, when the funding account is stored in the CLI, run: + +```bash copy +genlayer account send 0xYourOperatorAddress... 10gen +``` + +The required buffer depends on network activity and gas prices. Monitor and top it up before it runs out. Verify the operator balance on the node's target network: + +```bash copy +genlayer balances --beneficiary 0xYourOperatorAddress... +``` + + + A zero operator balance prevents priming and other consensus transactions even + when the Validator Wallet has enough stake. The node can remain synced while + missing validator duties, so verify this balance independently of sync health. + + ### Verify Your Validator After completing the wizard, verify your status: @@ -599,8 +621,9 @@ Once you have configured everything, you are ready to start the node. **Important:** Before starting the node, ensure you have: 1. **Operator key** imported into the node (see "Import the Operator Key" above) 2. **Validator wallet address** - obtained after joining as validator via `genlayer staking wizard` or `validator-join` + 3. **Funded operator address** - the configured operator must hold liquid GEN for priming and other validator transaction gas (see "Fund the Operator Wallet" above) - Without both configured, your node will run as a full node instead of a validator. + Without the operator key and Validator Wallet configured, your node will run as a full node instead of a validator. Without operator gas, it cannot submit validator duties. #### Running the Node using the binary