# Quiver — full documentation > Verifiable on-chain randomness (VRF / RNG) for Robinhood Chain. Quiver is a two-party commit–reveal entropy protocol: the result is keccak256(userRandom ‖ providerReveal ‖ blockHash?), where both values are committed before either is revealed, so neither the requester nor the provider can bias the outcome. A fair result holds as long as either party is honest. Source: https://quiver.foundation/docs · Generated from the Quiver documentation. Pages appear in reading order. Each section notes its canonical source URL. ============================================================================== # SOURCE: https://quiver.foundation/docs/index.md (Overview) ============================================================================== # Quiver documentation **Verifiable on-chain randomness for Robinhood Chain** — a two-party commit–reveal entropy protocol. Start here. ## Read in order 1. **[Architecture](https://quiver.foundation/docs/architecture.md)** — the components, roles, and request flows. 2. **[Protocol design](https://quiver.foundation/docs/protocol-design.md)** — how randomness is produced and why it's secure (the hash-chain / anchor mechanism, in depth). 3. **[Integration guide](https://quiver.foundation/docs/integration-guide.md)** — consume randomness from a contract (push & pull) or from TypeScript. 4. **[API reference](https://quiver.foundation/docs/api-reference.md)** — every function, event, error, and type. 5. **[Security model](https://quiver.foundation/docs/security.md)** — trust assumptions, threat model, mitigations. 6. **[Deployment runbook](https://quiver.foundation/docs/deployment.md)** — testnet → mainnet, step by step. 7. **[Keeper operations](https://quiver.foundation/docs/fletcher-operations.md)** — running the keeper (Fletcher) in production. ## Concepts | Term | Meaning | | --- | --- | | **Coordinator** | The on-chain contract (`QuiverCoordinator`) — verifies reveals, combines the two values, delivers callbacks | | **Provider** | Registers a hash-chain commitment and serves the random values behind it | | **Keeper** | The provider's off-chain service (`Fletcher`) that watches for requests and submits reveals | | **Requester / Consumer** | A contract or account that requests randomness | | **Sequence number** | The id assigned to each request; used to track it through fulfillment | | **Commitment** | The tip of the provider's keccak hash chain, `chain[N]` | | **Push flow** | `requestWithCallback` → keeper `revealWithCallback` → your `quiverCallback` | | **Pull flow** | `request` → you `reveal` and read the result | ## The one-line mental model ``` randomNumber = keccak256(userRandom ‖ providerReveal ‖ blockHash?) ``` Two values, each committed before either is revealed. Neither party can bias the result; a fair outcome holds as long as one party is honest. ## Try it ```bash forge test # 38 tests (unit, adversarial, fuzz) ./demo/local-demo.sh # full end-to-end on a local node ``` ============================================================================== # SOURCE: https://quiver.foundation/docs/architecture.md (Architecture) ============================================================================== # Quiver architecture A map of the moving parts and how a randomness request flows through them. For the cryptographic mechanism see [protocol-design.md](https://quiver.foundation/docs/protocol-design.md). --- ## Components | Component | Layer | Role | | --- | --- | --- | | `QuiverCoordinator` | on-chain | The protocol core: registers providers, escrows fees, stores requests, verifies reveals, delivers callbacks | | `QuiverConsumer` / `IQuiverConsumer` | on-chain | Base + interface for contracts that receive push-flow randomness | | Examples (`CoinFlip`, `DiceGame`) | on-chain | Reference consumer integrations | | `@quiver/sdk` | off-chain (TS) | Client for requesting/tracking randomness; hash-chain utilities; chain + ABI definitions | | `@quiver/fletcher` | off-chain (TS) | The provider's keeper: holds the seed, watches requests, submits reveals | | Deploy scripts | tooling | Foundry scripts for deploy + provider registration | ## Roles - **Provider** — commits a hash chain and operates a keeper (Fletcher). Earns a per-request fee. - **Requester / Consumer** — a contract or account that requests randomness and receives it back (via callback or self-reveal). - **Coordinator owner** — protocol admin: sets the protocol fee, can pause new requests. Cannot touch seeds or in-flight randomness. ## Push-flow sequence ``` Consumer QuiverCoordinator Fletcher (provider keeper) │ │ │ │ requestWithCallback │ │ │───────────────────────▶│ store Request(seq) │ │ │ emit RandomnessRequested │ │ │─────────────────────────▶│ (event: seq, userRandom) │ │ │ compute value = chain[N-seq] │ │ revealWithCallback │ (simulate, then send) │ │◀─────────────────────────│ │ quiverCallback(rnd) │ verify + combine │ │◀───────────────────────│ (delete req, advance) │ │ _fulfillRandomness │ emit RandomnessRevealed │ │ │ + CallbackSucceeded │ ``` If `quiverCallback` reverts, the coordinator emits `CallbackFailed` and buffers `rnd`; anyone can call `retryCallback` later. The provider is paid and unblocked regardless. ## Pull-flow sequence ``` Requester QuiverCoordinator Provider endpoint │ request(commit) │ │ │──────────────────────────▶│ store Request(seq) │ │ │ │ │ fetch value for seq ─────┼───────────────────────▶│ (off-chain) │◀──────────────────────────┼────────────────────────│ │ reveal(userRandom, value)│ verify + combine │ │──────────────────────────▶│ returns randomNumber │ ``` ## On-chain state - `providers[address] → ProviderInfo` — commitment tip, moving anchor, sequence counters, fee, chain length, fee manager, metadata. - `requests[keccak(provider, seq)] → Request` — the request's commitments, anchor, `numHashes`, requester, flags. Deleted on reveal. - `failedCallbacks[keccak(provider, seq)] → FailedCallback` — buffered randomness for retry. - Protocol fee + accrued protocol fees. The design keeps per-request storage to ~4 slots and reveal cost to O(outstanding requests) hashes (usually 1). See [protocol-design.md](https://quiver.foundation/docs/protocol-design.md) §4. ## Trust boundary summary ``` ┌───────────────────────── on-chain (trustless) ─────────────────────────┐ │ QuiverCoordinator: verifies every reveal, combines committed values, │ │ cannot be made to produce a biased result. │ └────────────────────────────────────────────────────────────────────────┘ ▲ ▲ commit (tip)│ commit (hash)│ ┌───────────┴───────────┐ ┌──────────┴───────────┐ │ Provider + Fletcher │ │ Requester / Consumer │ │ (trusted for liveness │ │ (trusted for its own │ │ & seed secrecy only) │ │ liveness only) │ └────────────────────────┘ └──────────────────────┘ ``` Neither off-chain party is trusted for *fairness* — only for *liveness*. See [security.md](https://quiver.foundation/docs/security.md). ============================================================================== # SOURCE: https://quiver.foundation/docs/protocol-design.md (Protocol design) ============================================================================== # Quiver protocol design This document explains **how Quiver produces verifiable randomness** and **why it is secure**. It is the reference for auditors and integrators who want to understand the mechanism rather than just call the API. Quiver is an on-chain adaptation of the classic two-party **commit–reveal** protocol, optimized with a **hash chain** so a provider can serve an effectively unbounded stream of requests from a single on-chain commitment. This is the design pioneered by [Pyth Entropy](https://docs.pyth.network/entropy/protocol-design); Quiver is an independent implementation branded and tuned for Robinhood Chain. --- ## 1. The problem with "just use blockhash" Randomness derived from a single on-chain source that a privileged actor controls is biasable: - `blockhash` / `block.prevrandao` are chosen (or at least influenced) by the block producer, who can re-roll by discarding blocks they dislike. - A single-party oracle that "posts a random number" can pick whichever value benefits it, or front-run/withhold based on the outcome. The fix is to require **two independent contributions**, each **committed before either value is known**, so that: - the result is a deterministic function of both contributions, and - neither party can change its contribution after seeing the other's. Then a fair outcome holds as long as **at least one** contributor is honest. ## 2. Commitments ### The provider's hash chain A provider picks a secret 32-byte `seed` and a chain length `N`, then computes ``` chain[0] = seed chain[i] = keccak256(chain[i-1]) for i = 1 … N ``` It publishes **only the tip**, `commitment = chain[N] = keccak256ᴺ(seed)`, via `register(...)`. Every other value stays secret. Only the tip `chain[N]` is public. The value the provider reveals for the *k-th* request it serves is a pre-image deeper in the chain: ``` revelation(k) = chain[N - k] ``` Why this works: because `keccak256` is one-way, nobody can compute `chain[N-1]`, `chain[N-2]`, … from the public `chain[N]`. But **verification is trivial**: given a claimed `revelation(k) = chain[N-k]`, hashing it `k` times must reproduce the tip: ``` keccak256ᵏ(chain[N-k]) == chain[N] == commitment ✓ ``` The provider therefore cannot lie about a revealed value — any wrong value fails the hash check — and cannot change it later, because it's fixed the moment the tip is published. ### The requester's contribution The requester picks its own value `userRandomNumber` and commits `userCommitment = keccak256(userRandomNumber)`. It reveals the pre-image only at reveal time (pull flow) or supplies it up-front (push flow — see §6). ## 3. Producing the random number When **both** revelations are on-chain, the coordinator computes: ```solidity randomNumber = keccak256(abi.encodePacked(userRandom, providerRevelation, blockHash)); ``` `blockHash` is `blockhash(requestBlock)` when the requester set `useBlockhash`, else `bytes32(0)`. Because `keccak256` is a random oracle over inputs neither party fully controlled, the output is uniform and unpredictable to both. ## 4. Bounded-gas verification (the anchor trick) Verifying request `k` naïvely costs `k` hashes — unbounded as the chain is consumed. Quiver keeps reveal gas **proportional only to the number of outstanding requests**. Each provider record tracks a moving **anchor**: - `currentCommitment` — the deepest revealed chain value so far, `chain[N - b]` - `currentCommitmentSequenceNumber` (`b`) — the sequence number that anchor corresponds to When a request is assigned sequence number `s`, it **stores its own anchor**: ``` req.providerCommitment = currentCommitment // chain[N-b] at request time req.numHashes = s - b // hashes needed to verify ``` At reveal, the coordinator checks `keccak256^(req.numHashes)(providerRevelation) == req.providerCommitment`. On success it advances the anchor forward (`currentCommitment ← providerRevelation`, `b ← s`) **iff** `s` is ahead of the current anchor. So: - **Sequential reveals** cost exactly **1 hash** each. - **Out-of-order reveals** still verify against each request's *stored* anchor, and never corrupt the moving anchor (it only ever moves forward). - The maximum work for any reveal is bounded by `maxNumHashes`, enforced at request time (`s - b ≤ maxNumHashes`), which also protects revealers from gas griefing. > This is why a request stores `numHashes` and a 32-byte anchor rather than re-deriving > from the tip: it makes every reveal self-contained and order-independent. ### Worked example (`N = 8`) | Action | `sequenceNumber` (next) | anchor `b` | `currentCommitment` | | ------------------------------ | ----------------------- | ---------- | ------------------- | | `register(commitment=chain[8])`| 1 | 0 | `chain[8]` | | request → seq 1 (anchor 0, 1 hash) | 2 | 0 | `chain[8]` | | reveal seq 1 → `chain[7]` | 2 | 1 | `chain[7]` | | request → seq 2 (anchor 1, 1 hash) | 3 | 1 | `chain[7]` | | reveal seq 2 → `chain[6]` | 3 | 2 | `chain[6]` | Reveal seq 1 checks `keccak(chain[7]) == chain[8]`; reveal seq 2 checks `keccak(chain[6]) == chain[7]`. Each is one hash. ## 5. Rotation A chain is finite (`N` values). A provider extends or replaces it with `rotateCommitment(newCommitment, …, newChainLength, …)`: - The new chain is anchored just below the next sequence number (`anchor = sequenceNumber - 1`), so the very next request verifies in one hash. - **In-flight requests are unaffected** — they carry their own stored anchors from the old chain and still verify and reveal normally. - Rotation is also the recovery path if a `seed` is suspected compromised: rotate to a fresh seed and all *future* values are safe again. Fletcher tracks chain **segments** (each `{seed, anchorSequenceNumber, length}`) so it can still reveal old-segment requests after a rotation. ## 6. Two flows: pull and push ### Pull flow — `request` → `reveal` 1. Requester commits `userCommitment = keccak256(userRandom)`, keeping `userRandom` secret. 2. `request(provider, userCommitment, useBlockhash)` reserves sequence `s`. 3. Requester obtains `providerRevelation` for `s` (from the provider's endpoint) and calls `reveal(provider, s, userRandom, providerRevelation)`, which returns the randomness. The requester's value stays secret until it chooses to reveal, so it is a genuine independent commitment. Optionally fold in `blockhash` for extra provider-unpredictability. ### Push flow — `requestWithCallback` → `revealWithCallback` → `quiverCallback` 1. `requestWithCallback(provider, userRandom)` — the raw `userRandom` is passed and the coordinator stores `keccak256(userRandom)`. It is **emitted in the event** so the keeper can fulfill. 2. The keeper observes `RandomnessRequested`, computes the provider's reveal, and calls `revealWithCallback(provider, s, userRandom, providerRevelation)`. 3. The coordinator produces the randomness and invokes your contract's `quiverCallback(s, provider, randomNumber)`. **Is emitting `userRandom` a problem?** No. Protocol safety rests on the provider's revealed value being committed *before* the request — the provider cannot re-choose it. The requester's value contributing entropy does **not** require secrecy in the push flow. The one residual consideration: because `useBlockhash` is off in the push flow, the provider *can compute* the outcome once it sees the request, and could choose to withhold the reveal (liveness, not bias). Mitigations are covered in [security.md](https://quiver.foundation/docs/security.md) (reputation/staking, using the pull flow with `useBlockhash`, and callback buffering so a griefing consumer can't block a provider). ## 7. Callback safety `revealWithCallback` performs all state changes (advance anchor, delete the request) **before** calling the consumer, and wraps the call in `try/catch`: - **Success** → `CallbackSucceeded`. - **Revert / out-of-gas** → the randomness is written to a retry buffer and `CallbackFailed` is emitted. Anyone can later call `retryCallback(provider, s)` to re-deliver it. The provider is paid and unblocked regardless of a misbehaving consumer. This makes fulfillment robust: a buggy or malicious consumer harms only itself. ## 8. What Quiver guarantees — and what it doesn't **Guarantees (given an honest coordinator deployment):** - The output is a fixed function of two pre-committed values; **neither party alone can bias it**. - A provider cannot forge or alter a revealed value (hash-chain verification). - A reveal cannot be replayed (the request is deleted on reveal). - Randomness for a failed callback is never lost (retry buffer). **Non-guarantees / trust assumptions:** - **Liveness** depends on at least one party revealing. A provider (or, in the pull flow, a requester) can refuse — this cannot bias results but can delay them. - The **push flow without `useBlockhash`** lets the provider predict (not change) the outcome, enabling selective withholding. Use provider reputation/staking, or the pull flow with `useBlockhash`, where stronger guarantees are needed. - Security assumes `keccak256` pre-image and collision resistance, and a provider that keeps its `seed` secret until each reveal. See [security.md](https://quiver.foundation/docs/security.md) for the full threat model and mitigations. ============================================================================== # SOURCE: https://quiver.foundation/docs/integration-guide.md (Integration guide) ============================================================================== # Integrating with Quiver This guide shows how to consume Quiver randomness from a smart contract (push and pull flows) and from off-chain TypeScript. For the mechanism behind it, see [protocol-design.md](https://quiver.foundation/docs/protocol-design.md). --- ## Choosing a flow | | **Push** (`requestWithCallback`) | **Pull** (`request` → `reveal`) | | --- | --- | --- | | Delivery | Coordinator calls your `quiverCallback` | You call `reveal` and read the return value | | Who fulfills | The provider's Fletcher keeper | You (or anyone with both secrets) | | Your value secret? | No (emitted for the keeper) | Yes, until you reveal | | `blockhash` mixing | No | Optional | | Best for | Games, mints, most dapps (hands-off UX) | Max trust-minimization; you run your own reveal | Most integrations want the **push flow**. Use the **pull flow** when you need the requester's contribution to stay secret until reveal and/or want `blockhash` folded in. --- ## Push flow (recommended) ### 1. Inherit `QuiverConsumer` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {QuiverConsumer} from "quiver/QuiverConsumer.sol"; contract Lottery is QuiverConsumer { constructor(address quiver, address provider) QuiverConsumer(quiver, provider) {} // Draw randomness. `msg.value` (or the contract's balance) must cover the fee. function drawWinner(bytes32 userRandom) external returns (uint64 seq) { seq = _requestRandomness(userRandom); // record `seq -> round` so the callback can resolve the right draw } // Called ONLY by the coordinator. Put your logic here. function _fulfillRandomness(uint64 seq, address /*provider*/, bytes32 rnd) internal override { uint256 winnerIndex = uint256(rnd) % participants.length; // settle the round with `winnerIndex` } } ``` The base contract handles the coordinator-only check on the callback, quoting/paying the fee, and dispatching to `_fulfillRandomness`. You never write the raw `quiverCallback`. ### 2. Fund the fee `_requestRandomness` calls `requestWithCallback{value: fee}` using **the consumer contract's balance**. Ensure the contract holds enough ETH, or forward `msg.value` in your request function and keep a buffer. Read the current fee with `_randomnessFee()`. ### 3. Generate a good `userRandomNumber` Pass fresh, unpredictable 32 bytes each request. Off-chain callers should use a CSPRNG (`QuiverClient.generateUserRandom()`); on-chain, derive from values not known to the provider in advance where possible. Even a weak user value cannot let the *provider* bias the result, but a predictable one weakens your independent contribution. ### Callback rules (important) - **Keep it lean and non-reverting.** If `_fulfillRandomness` reverts or runs out of gas, the coordinator buffers the randomness and emits `CallbackFailed`; recover it later via `retryCallback(provider, seq)`. Don't rely on this as normal flow. - **Be idempotent / defensive.** Ignore unknown or already-resolved sequence numbers (see the `CoinFlip`/`DiceGame` examples), since `retryCallback` can re-invoke you. - **Never trust `tx.origin` or re-enter assumptions.** Only `msg.sender == coordinator` is guaranteed (enforced for you by the base contract). --- ## Pull flow Use the coordinator directly (or the SDK). You keep `userRandom` secret and reveal it yourself. ```solidity import {IQuiverCoordinator} from "quiver/interfaces/IQuiverCoordinator.sol"; // 1. Commit bytes32 userRandom = /* your secret 32 bytes */; bytes32 commitment = coordinator.constructUserCommitment(userRandom); uint64 seq = coordinator.request{value: coordinator.getFee(provider)}(provider, commitment, true /*useBlockhash*/); // 2. Fetch providerRevelation for `seq` from the provider's endpoint (off-chain), // then reveal: bytes32 rnd = coordinator.reveal(provider, seq, userRandom, providerRevelation); ``` With `useBlockhash = true`, reveal within 256 blocks of the request (older block hashes are unavailable on-chain and would fold in `bytes32(0)`). --- ## Off-chain with `@quiver/sdk` ```ts import {QuiverClient, robinhoodTestnet} from "@quiver/sdk"; import {createPublicClient, createWalletClient, http} from "viem"; import {privateKeyToAccount} from "viem/accounts"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const publicClient = createPublicClient({chain: robinhoodTestnet, transport: http()}); const walletClient = createWalletClient({account, chain: robinhoodTestnet, transport: http()}); const quiver = new QuiverClient({coordinator, publicClient, walletClient}); // Push flow: request, then await the RandomnessRevealed event const {sequenceNumber} = await quiver.requestRandomness(provider); const {randomNumber} = await quiver.waitForFulfillment(provider, sequenceNumber); // Pull flow: request, then reveal yourself const userRandom = QuiverClient.generateUserRandom(); const {sequenceNumber: s} = await quiver.request(provider, userRandom, {useBlockhash: true}); const providerRevelation = /* from the provider's endpoint */; const {randomNumber: rnd} = await quiver.reveal(provider, s, userRandom, providerRevelation); ``` Hash-chain helpers (`commitmentOf`, `revelationOf`, `combineRandomValues`, `userCommitmentOf`) are also exported for tooling and tests. --- ## Testing your integration (Foundry) You don't need a live provider to test — build the chain in-process with the test helper and reveal manually. See `test/examples/Examples.t.sol` for a complete pattern: ```solidity import {HashChainLib} from "quiver-test/helpers/HashChain.sol"; // or copy it in bytes32 seed = keccak256("test-seed"); uint64 N = 64; vm.prank(provider); coordinator.register(0, HashChainLib.commitment(seed, N), "", N, 100, ""); // request via your consumer, then fulfill as the provider would: coordinator.revealWithCallback(provider, seq, userRandom, HashChainLib.revelation(seed, N, seq)); ``` --- ## Common pitfalls - **Fee changes.** Quote `getFee` immediately before requesting; providers can update fees. The SDK does this for you. Overpayment is refunded by the coordinator. - **`vm.prank` + external helper.** In tests, computing the commitment via an external call to the coordinator consumes a pending `vm.prank`. Compute it locally (`keccak256(abi.encodePacked(userRandom))`) instead. - **Assuming instant fulfillment.** The push flow is asynchronous — design your UX around a pending → resolved lifecycle keyed by the sequence number. - **Reusing a `userRandomNumber`.** Use a fresh value per request. ============================================================================== # SOURCE: https://quiver.foundation/docs/agent-integration.md (Agent integration) ============================================================================== # Agent integration (Claude Code skill) Quiver ships a **Claude Code skill** so an AI agent can integrate verifiable on-chain randomness for you — write a `QuiverConsumer` contract, wire up the push/pull flow, use the `@quiver/sdk` client, or run a Fletcher keeper. The skill carries the verified coordinator/provider addresses, the safety rules for `_fulfillRandomness`, and the full SDK surface, so an agent produces correct integration code without guessing. Everything below is public and self-contained. Pick whichever install path fits your setup. ## Option A — install as a Claude Code plugin (recommended) Add the Quiver marketplace once, then install the plugin: ``` /plugin marketplace add camdengrieh/quiver-kit /plugin install quiver@quiver ``` This installs the `quiver-integration` skill (plus its reference files). Your agent will invoke it automatically whenever you ask it to add randomness/VRF/RNG to a contract or dApp on Robinhood Chain. ## Option B — drop the skill into a project Copy the skill straight into a repo's `.claude/skills/` directory: ```bash mkdir -p .claude/skills/quiver-integration/references base=https://quiver.foundation/skills/quiver-integration curl -sfL $base/SKILL.md -o .claude/skills/quiver-integration/SKILL.md for f in consumer-contract typescript-sdk keeper-and-provider networks; do curl -sfL $base/references/$f.md -o .claude/skills/quiver-integration/references/$f.md done ``` The skill is now active for any agent working in that project. ## Option C — point any agent at the hosted files Every file is fetchable directly, so an agent (Claude Code, an SDK app, or any tool that can read a URL) can pull it on demand: - Skill entrypoint: [`/skills/quiver-integration/SKILL.md`](https://quiver.foundation/skills/quiver-integration/SKILL.md) - References: [`consumer-contract.md`](https://quiver.foundation/skills/quiver-integration/references/consumer-contract.md) · [`typescript-sdk.md`](https://quiver.foundation/skills/quiver-integration/references/typescript-sdk.md) · [`keeper-and-provider.md`](https://quiver.foundation/skills/quiver-integration/references/keeper-and-provider.md) · [`networks.md`](https://quiver.foundation/skills/quiver-integration/references/networks.md) - Machine index of the skill bundle: [`/skills/quiver-integration/skill.json`](https://quiver.foundation/skills/quiver-integration/skill.json) For general LLM context (not the skill), the [`llms.txt`](https://quiver.foundation/llms.txt) and [`llms-full.txt`](https://quiver.foundation/llms-full.txt) indexes cover the whole documentation set. ## What the skill knows - **Push (callback) flow** — inherit `QuiverConsumer`, request in your entrypoint, settle in `_fulfillRandomness`, and the keeper delivers the result. Includes the non-negotiable rules (never revert on the happy path, correlate by sequence number, fund the contract for fees). - **Pull flow** — request with a sealed commitment and reveal yourself, no keeper required. - **TypeScript** — the `@quiver/sdk` `QuiverClient` (`requestRandomness`, `request`, `reveal`, `waitForFulfillment`) and hash-chain helpers. - **Keeper & provider ops** — running Fletcher and registering/rotating a hash-chain provider. - **Verified network constants** — coordinator + default provider addresses and fees for mainnet (chainId 4663) and testnet (chainId 46630). ## Try it Ask your agent something like: > "Add a Quiver-powered raffle to my Solidity project on Robinhood Chain testnet — one entry per > address, pick a winner with verifiable randomness." With the skill installed it will scaffold a `QuiverConsumer`, wire the request/callback, and plug in the correct testnet coordinator and provider addresses. ============================================================================== # SOURCE: https://quiver.foundation/docs/api-reference.md (API reference) ============================================================================== # Quiver API reference Complete reference for the on-chain interface (`IQuiverCoordinator`), the consumer callback (`IQuiverConsumer` / `QuiverConsumer`), the data types, events, and errors. Signatures are authoritative in `src/interfaces/`. --- ## `IQuiverCoordinator` ### Randomness — requester functions #### `request(address provider, bytes32 userCommitment, bool useBlockhash) payable → uint64` Pull flow. Reserves the next sequence number from `provider`. `userCommitment` is `keccak256(userRandomNumber)`; keep `userRandomNumber` secret. Set `useBlockhash` to fold `blockhash(requestBlock)` into the result (reveal within 256 blocks). Requires `msg.value ≥ getFee(provider)`; overpayment is refunded. Returns the assigned sequence number. Reverts: `ProviderNotRegistered`, `ProviderChainExhausted`, `TooManyHashes`, `InsufficientFee`. Blocked while paused. #### `requestWithCallback(address provider, bytes32 userRandomNumber) payable → uint64` Push flow. Commits `keccak256(userRandomNumber)` and emits the raw `userRandomNumber` for the keeper. The caller should implement `IQuiverConsumer`. Same fee rules and reverts as `request`. Returns the assigned sequence number. #### `reveal(address provider, uint64 sequenceNumber, bytes32 userRevelation, bytes32 providerRevelation) → bytes32` Completes a pull-flow request and returns the random number. Verifies both revelations, advances the provider anchor, deletes the request. Reverts: `NoSuchRequest`, `RequestCallbackMismatch(true)` (if it was a callback request), `IncorrectUserRevelation`, `IncorrectProviderRevelation`. `nonReentrant`. #### `revealWithCallback(address provider, uint64 sequenceNumber, bytes32 userRandomNumber, bytes32 providerRevelation)` Completes a push-flow request and delivers randomness to the requester's `quiverCallback`. On callback revert/OOG, buffers the value (`CallbackFailed`) instead of reverting the whole tx. Reverts: `NoSuchRequest`, `RequestCallbackMismatch(false)`, `IncorrectUserRevelation`, `IncorrectProviderRevelation`. `nonReentrant`. #### `retryCallback(address provider, uint64 sequenceNumber)` Re-delivers buffered randomness for a previously-failed callback. Reverts `NoFailedCallback` if none. If the callback reverts again, the whole tx reverts and the buffer is preserved. `nonReentrant`. ### Provider management #### `register(uint128 feeInWei, bytes32 commitment, bytes commitmentMetadata, uint64 chainLength, uint64 maxNumHashes, bytes uri)` Registers `msg.sender` as a provider. `commitment = keccak256^chainLength(seed)`. `chainLength ≥ 2`; `1 ≤ maxNumHashes ≤ 10_000`. Reverts `ProviderAlreadyRegistered`, `InvalidChainLength`, `InvalidMaxNumHashes`. #### `rotateCommitment(bytes32 newCommitment, bytes commitmentMetadata, uint64 newChainLength, bytes uri)` Rotates `msg.sender` onto a fresh chain, anchored at `sequenceNumber - 1`. In-flight requests are unaffected. Reverts `ProviderNotRegistered`, `InvalidChainLength`. #### `setProviderFee(uint128)` / `setProviderFeeAsFeeManager(address, uint128)` Update a provider's per-request fee — by the provider itself, or its delegated fee manager. Emits `ProviderFeeUpdated`. #### `setFeeManager(address feeManager)` Delegate fee-setting and withdrawals for `msg.sender`'s provider. Emits `ProviderFeeManagerUpdated`. #### `withdraw(uint128 amount)` / `withdrawAsFeeManager(address provider, uint128 amount)` Withdraw accrued provider fees to `msg.sender`. Reverts `InsufficientAccruedFees`, `NotFeeManager`. `nonReentrant`. ### Administration (`onlyOwner`) | Function | Effect | | --- | --- | | `setProtocolFee(uint128)` | Set the protocol fee added to every request | | `withdrawProtocolFees(address to, uint128 amount)` | Withdraw accrued protocol fees | | `pause()` / `unpause()` | Halt / resume **new requests** (reveals stay enabled) | Ownership is `Ownable2Step` — transfers require the new owner to accept. ### Views | Function | Returns | | --- | --- | | `getFee(address provider)` | provider fee + protocol fee (wei) | | `getProtocolFee()` | protocol fee component | | `getAccruedProtocolFees()` | withdrawable protocol fees | | `getProviderInfo(address)` | full `ProviderInfo` struct | | `getProviderSequenceNumber(address)` | next sequence number | | `getRequest(address, uint64)` | the `Request` struct (zeroed if none) | | `getFailedCallback(address, uint64)` | `(bool exists, bytes32 randomNumber)` | | `constructUserCommitment(bytes32)` | `keccak256(abi.encodePacked(userRandomNumber))` | | `combineRandomValues(bytes32,bytes32,bytes32)` | `keccak256(abi.encodePacked(u,p,b))` | ### Constants | Name | Value | Meaning | | --- | --- | --- | | `MAX_NUM_HASHES` | `10_000` | ceiling on a provider's `maxNumHashes` | | `MIN_CHAIN_LENGTH` | `2` | minimum registrable chain length | --- ## `IQuiverConsumer` #### `quiverCallback(uint64 sequenceNumber, address provider, bytes32 randomNumber)` Implement to receive push-flow randomness. **Must** require `msg.sender == coordinator`. Prefer inheriting `QuiverConsumer`, which enforces this and dispatches to `_fulfillRandomness`. ## `QuiverConsumer` (abstract base) | Member | Purpose | | --- | --- | | `constructor(address quiver, address provider)` | Bind coordinator + default provider | | `_requestRandomness(bytes32 userRandom) → uint64` | Draw from the default provider, paying the fee | | `_requestRandomness(address provider, bytes32 userRandom) → uint64` | Draw from a specific provider | | `_fulfillRandomness(uint64 seq, address provider, bytes32 rnd)` | **override** — your logic | | `_randomnessFee() → uint128` | Fee for the default provider | | `getCoordinator() → address` / `getProvider() → address` | Bound addresses | --- ## Data types (`QuiverStructs`) ### `ProviderInfo` `feeInWei`, `accruedFeesInWei`, `originalCommitment`, `currentCommitment`, `originalChainLength`, `currentCommitmentSequenceNumber`, `sequenceNumber`, `endSequenceNumber`, `maxNumHashes`, `feeManager`, `uri`, `commitmentMetadata`. ### `Request` `provider`, `sequenceNumber`, `numHashes`, `userCommitment`, `providerCommitment` (verification anchor), `requester`, `blockNumber`, `useBlockhash`, `isRequestWithCallback`. A zero `provider` means "no active request". ### `FailedCallback` `randomNumber`, `requester`, `exists`. --- ## Events | Event | When | | --- | --- | | `ProviderRegistered(provider, feeInWei, commitment, chainLength, maxNumHashes)` | `register` | | `ProviderCommitmentRotated(provider, newCommitment, newChainLength, anchorSequenceNumber)` | `rotateCommitment` | | `ProviderFeeUpdated(provider, oldFeeInWei, newFeeInWei)` | fee change | | `ProviderFeeManagerUpdated(provider, feeManager)` | delegate change | | `ProviderWithdrawal(provider, to, amount)` | fee withdrawal | | `RandomnessRequested(provider, requester, sequenceNumber, userContribution, numHashes, withCallback, useBlockhash)` | a request. `userContribution` = commitment (pull) or raw value (callback) | | `RandomnessRevealed(provider, sequenceNumber, requester, randomNumber, userRevelation, providerRevelation)` | a reveal | | `CallbackSucceeded(provider, sequenceNumber, requester, randomNumber)` | callback delivered | | `CallbackFailed(provider, sequenceNumber, requester, randomNumber, reason)` | callback reverted → buffered | | `ProtocolFeeUpdated(oldFeeInWei, newFeeInWei)` / `ProtocolFeeWithdrawal(to, amount)` | admin fee ops | Indexed fields: provider / requester / sequenceNumber are indexed on the core events for efficient filtering. --- ## Errors | Error | Meaning | | --- | --- | | `ProviderNotRegistered(provider)` | provider unknown | | `ProviderAlreadyRegistered(provider)` | double `register` — use `rotateCommitment` | | `InvalidChainLength(chainLength)` | `< MIN_CHAIN_LENGTH` | | `InvalidMaxNumHashes(maxNumHashes)` | `0` or `> MAX_NUM_HASHES` | | `ProviderChainExhausted(provider, endSequenceNumber)` | chain fully consumed — rotate | | `TooManyHashes(required, maxNumHashes)` | too many outstanding requests; provider must reveal | | `NoSuchRequest(provider, sequenceNumber)` | no active request (or already revealed) | | `IncorrectUserRevelation()` | user revelation ≠ commitment | | `IncorrectProviderRevelation()` | provider revelation doesn't chain to the anchor | | `RequestCallbackMismatch(isRequestWithCallback)` | wrong reveal function for the request type | | `NoFailedCallback(provider, sequenceNumber)` | nothing buffered to retry | | `InsufficientFee(provided, required)` | `msg.value` too low | | `InsufficientAccruedFees(requested, available)` | over-withdrawal | | `TransferFailed(to, amount)` | native transfer failed | | `NotFeeManager(caller, provider)` | not the delegated fee manager | | `ZeroAddress()` | zero address where non-zero required | --- ## SDK surface (`@quiver/sdk`) - `QuiverClient` — `getFee`, `getProviderInfo`, `getRequest`, `getFailedCallback`, `request`, `requestRandomness`, `reveal`, `waitForFulfillment`; static `generateUserRandom`, `userCommitment`. - Hash-chain utils — `buildQuiver`, `commitmentOf`, `revelationOf`, `userCommitmentOf`, `combineRandomValues`. - Chains — `robinhoodMainnet`, `robinhoodTestnet`, `anvilLocal`, `QUIVER_CHAINS`. - ABIs — `quiverCoordinatorAbi`, `quiverConsumerAbi`. ============================================================================== # SOURCE: https://quiver.foundation/docs/security.md (Security model) ============================================================================== # Quiver security model This document states Quiver's **trust assumptions**, the **threat model**, and the **mitigations** for each attack. Read it before deploying with real value. > **Status:** Quiver has a thorough test suite (unit, adversarial, fuzz) but has **not** > had an independent third-party audit. Treat the assumptions below as load-bearing. --- ## 1. Actors and trust | Actor | Trusted for | Can it bias randomness? | | --- | --- | --- | | **Coordinator** (contract) | Correct, immutable execution | No — it only combines committed values | | **Provider** | Keeping `seed` secret until each reveal; liveness | **No** (bias). Yes: withhold reveals (liveness) | | **Requester** | Its own contribution; liveness (pull flow) | **No** (bias). Yes: withhold its reveal (pull) | | **Coordinator owner** | Setting protocol fee, pausing new requests | No — cannot touch in-flight randomness or seeds | Core claim: **as long as at least one of {provider, requester} is honest, the output is unbiased and unpredictable.** The two contributions are each committed before either is revealed, and the result is a fixed function of both. ## 2. Cryptographic assumptions - `keccak256` is **pre-image resistant** — nobody can invert the published commitment `chain[N]` to learn earlier values, and nobody can find a value hashing to a required anchor other than the true pre-image. - `keccak256` is **collision resistant** — a provider cannot craft two distinct chains sharing a tip. - The combined output `keccak256(userRandom ‖ providerRevelation ‖ blockHash)` behaves as a random oracle over inputs neither party solely controls. ## 3. Threats and mitigations ### 3.1 Provider forging or altering a revealed value **Attempt:** submit a `providerRevelation` other than the committed `chain[N-k]`. **Mitigation:** the coordinator recomputes `keccak256^numHashes(providerRevelation)` and requires it to equal the request's stored anchor. Any other value fails. The chain is fixed at registration; the provider has no freedom. ✔ Enforced in `_verifyAndResolve`. ### 3.2 Requester submitting a mismatched revelation **Attempt:** reveal a `userRandom` different from the committed one to steer the result. **Mitigation:** `keccak256(userRandom)` must equal the stored `userCommitment`. ✔ ### 3.3 Replay / double-reveal **Attempt:** reveal the same request twice, or reuse a revelation. **Mitigation:** the request slot is `delete`d on reveal; a second reveal hits `NoSuchRequest`. Sequence numbers are monotonic and never reused. ✔ ### 3.4 Provider selective withholding (the main liveness risk) **Setup:** in the **push flow**, `useBlockhash` is off and the raw `userRandom` is public, so the provider can *compute* the outcome as soon as the request appears — and could decline to reveal outcomes it dislikes. **It cannot bias** (the value is fixed), but it can **stall**. **Mitigations:** - **Economic/reputation:** run providers you trust or that are staked/slashable; a provider that withholds is publicly observable (open request, no reveal). - **Pull flow + `useBlockhash`:** fold in `blockhash(requestBlock)`, unknown at request time, so the provider *cannot even predict* the outcome when deciding to reveal. - **Multiple providers:** consumers can choose among providers; a withholding provider loses fees and reputation. - **Timeouts (app-level):** design your consumer to allow re-requesting from another provider if a request isn't fulfilled within a deadline. ### 3.5 Requester withholding (pull flow) **Attempt:** a requester that dislikes the (secret-to-others) outcome never reveals. **Impact:** only its own request stalls; it already paid the fee. No effect on others. **Note:** the requester cannot know the outcome before revealing (it doesn't have the provider's value), so this is not even a useful attack — just abandonment. ### 3.6 Block-producer / sequencer manipulation **With `useBlockhash`:** on an Arbitrum Orbit chain the sequencer produces blocks and influences `blockhash`. Folding `blockhash` adds entropy the *provider* can't predict, but a malicious sequencer colluding with a party could grind block hashes. **Guidance:** `blockhash` is a *hardening* input, not the root of trust — the two-party commitment is. For adversarial-sequencer threat models, rely on the provider+user commitments (both fixed) and treat `blockhash` as optional defense-in-depth. Never make `blockhash` the *sole* entropy source. ### 3.7 Callback griefing **Attempt:** a consumer whose `quiverCallback` always reverts or burns gas, to block the provider's reveal tx / waste its gas. **Mitigation:** `revealWithCallback` completes all state changes first, then calls the consumer inside `try/catch`. A revert/out-of-gas is caught; the randomness is buffered (`CallbackFailed`) and the provider's tx still succeeds and earns the fee. The consumer recovers via `retryCallback`. The griefer harms only itself. Fletcher additionally simulates the reveal before sending. ✔ ### 3.8 Reentrancy **Surface:** the callback and native-token transfers (fee refunds, withdrawals). **Mitigations:** reveals, withdrawals, and `retryCallback` are `nonReentrant`; all follow checks-effects-interactions (request deleted / balances updated before external calls). `request` refunds overpayment **after** writing state and advancing the sequence number, so a reentrant request from the refund cannot reuse a sequence number. ✔ ### 3.9 Gas-griefing via long hash chains **Attempt:** force a reveal to compute an enormous number of hashes. **Mitigation:** `numHashes` is bounded at request time by the provider's `maxNumHashes` (itself capped by `MAX_NUM_HASHES = 10_000`). Prompt reveals keep it at 1. ✔ ### 3.10 Seed compromise **Impact:** anyone who learns a provider's `seed` can predict all its future values. **Mitigations:** treat the seed like a private key (see [fletcher-operations.md](https://quiver.foundation/docs/fletcher-operations.md)); on suspicion, **rotate immediately** (`rotateCommitment`) — all future values use a fresh seed and are safe again. In-flight requests on the old chain remain as-committed. ### 3.11 Admin powers The coordinator owner (`Ownable2Step`) can: - set the protocol fee (affects only *future* request pricing), - withdraw accrued protocol fees, - `pause()` **new requests** (in-flight reveals and retries stay enabled). The owner **cannot** alter seeds, forge values, change in-flight requests, or seize provider fees. Use a multisig/timelock for the owner in production, and consider renouncing pause once stable. Two-step ownership transfer prevents fat-fingering the owner to an inaccessible address. ## 4. Randomness quality notes - **Uniformity & range reduction.** The raw output is a uniform 256-bit value. Reduce to a range with modulo — the modulo bias for a small range over 2²⁵⁶ is ~`range / 2²⁵⁶`, i.e. cryptographically negligible. For a d6, bias ≈ 1/2²⁵³. - **Independence.** Each request consumes a distinct provider value and a distinct user value, so outputs are independent. Never reuse a `userRandomNumber` across requests. - **Do not derive multiple "independent" draws by re-hashing one output with attacker- known inputs.** If you need N independent values, request N times or expand with a domain-separated KDF over the single verifiable output. ## 5. Deployment hardening checklist - [ ] Owner is a multisig (and ideally behind a timelock). - [ ] Providers you rely on are ones you operate or that are economically accountable. - [ ] Consumers treat fulfillment as async and tolerate/retry withholding. - [ ] Consumer callbacks are lean, idempotent, and never assume they can't be retried. - [ ] Seeds are generated with a CSPRNG, stored like private keys, and rotation is rehearsed. - [ ] For high-value use, prefer the pull flow with `useBlockhash`, or add provider staking. - [ ] Independent audit before mainnet value at scale. ## 6. Responsible disclosure Found a vulnerability? Do not open a public issue. Contact the maintainers privately with a description and, ideally, a proof-of-concept. (Wire up a security contact / bug-bounty before mainnet.) ============================================================================== # SOURCE: https://quiver.foundation/docs/deployment.md (Deployment) ============================================================================== # Deploying Quiver to Robinhood Chain This is the runbook for deploying the coordinator, registering a provider, and running Fletcher — first on **testnet (46630)**, then **mainnet (4663)**. > Live broadcasts require a **funded key** and **RPC access** that only you control. This > repo scripts everything up to the broadcast; you supply the signer. --- ## 0. Networks | | Testnet | Mainnet | | --- | --- | --- | | Chain ID | `46630` | `4663` | | RPC (public, rate-limited) | `https://rpc.testnet.chain.robinhood.com` | `https://rpc.mainnet.chain.robinhood.com` | | RPC (recommended) | `https://robinhood-testnet.g.alchemy.com/v2/KEY` | `https://robinhood-mainnet.g.alchemy.com/v2/KEY` | | Explorer | explorer.testnet.chain.robinhood.com | robinhoodchain.blockscout.com | | Gas token | ETH | ETH | | Foundry profile name | `rh_testnet` | `rh_mainnet` | Robinhood Chain is an Arbitrum Orbit L2; standard Foundry tooling works unmodified. ## 1. Prerequisites ```bash forge --version # Foundry node --version # ≥ 20 pnpm --version forge install && pnpm install forge test # green before you deploy anything ``` Fund your deployer address with a little ETH on the target network (bridge to testnet/mainnet per Robinhood's docs). ## 2. Configure environment ```bash cp .env.example .env # edit .env — at minimum set RH_TESTNET_RPC_URL / RH_MAINNET_RPC_URL ``` **Use a keystore, not a raw key**, especially for mainnet: ```bash cast wallet import quiver-deployer --interactive # paste your private key once # deploy with `--account quiver-deployer` (Foundry prompts for the password) ``` ## 3. Deploy the coordinator Dry run (no broadcast) first: ```bash forge script script/Deploy.s.sol --rpc-url rh_testnet ``` Broadcast + verify on Blockscout: ```bash forge script script/Deploy.s.sol \ --rpc-url rh_testnet \ --account quiver-deployer \ --broadcast \ --verify --verifier blockscout \ --verifier-url https://explorer.testnet.chain.robinhood.com/api ``` This writes `deployments/46630.json` with the coordinator address. Commit it. > **Owner:** defaults to the deployer. For production set `QUIVER_OWNER` to a **multisig** > (`QUIVER_OWNER=0xSAFE forge script ...`). ## 4. Register a provider Pick a chain length (random values before a rotation) and generate a seed: ```bash pnpm --filter @quiver/fletcher exec tsx src/index.ts genseed # → 0x… , store as FLETCHER_SEED ``` Set in `.env`: `QUIVER_COORDINATOR_ADDRESS` (from step 3), `FLETCHER_SEED`, `FLETCHER_CHAIN_LENGTH` (e.g. `100000`), `FLETCHER_FEE_WEI`, `FLETCHER_MAX_NUM_HASHES`, `PRIVATE_KEY` (the provider key). Then either: ```bash # via Fletcher (recommended — same code path the keeper uses) FLETCHER_NETWORK=rh_testnet pnpm --filter @quiver/fletcher exec tsx src/index.ts register # …or via Foundry forge script script/RegisterProvider.s.sol --rpc-url rh_testnet --account quiver-deployer --broadcast ``` Both compute `commitment = keccak256^chainLength(seed)` identically. ## 5. Run Fletcher ```bash FLETCHER_NETWORK=rh_testnet pnpm --filter @quiver/fletcher exec tsx src/index.ts run ``` For production, run it as a managed service (systemd/Docker) with restart-on-failure and a dedicated, funded operator key. See [fletcher-operations.md](https://quiver.foundation/docs/fletcher-operations.md). ## 6. (Optional) Deploy an example consumer ```bash QUIVER_COORDINATOR_ADDRESS=0x… QUIVER_PROVIDER_ADDRESS=0x… \ forge script script/DeployCoinFlip.s.sol --rpc-url rh_testnet --account quiver-deployer --broadcast ``` ## 7. Smoke test on testnet ```bash # request a flip (as any funded account) cast send "flip(bytes32)" $(cast keccak $RANDOM) --account quiver-deployer --rpc-url rh_testnet # watch Fletcher fulfill, then read the result cast call "flips(uint64)(address,uint8,bool)" 1 --rpc-url rh_testnet ``` Or run the whole thing locally first: `./demo/local-demo.sh`. ## 8. Promote to mainnet Repeat steps 3–7 with `--rpc-url rh_mainnet` and the mainnet verifier URL (`https://robinhoodchain.blockscout.com/api`). Before mainnet: - [ ] Owner set to a multisig (ideally + timelock). - [ ] `deployments/4663.json` committed and published to integrators. - [ ] Fletcher running as a monitored service with alerting on low ETH and low remaining random values. - [ ] A **fresh** mainnet seed (never reuse the testnet seed). - [ ] Reviewed [security.md](https://quiver.foundation/docs/security.md); ideally an independent audit for value at scale. ## 9. Post-deploy checklist - [ ] `getProviderInfo(provider)` shows the expected commitment, fee, and chain length. - [ ] A test request is fulfilled end-to-end. - [ ] Contract is verified and readable on Blockscout. - [ ] Runbooks exist for: rotating a chain, pausing, transferring ownership, and rotating the Fletcher operator key. --- ### Troubleshooting | Symptom | Likely cause / fix | | --- | --- | | `ProviderNotRegistered` on request | run step 4; check you targeted the right provider address | | Fletcher: "provider … not registered" | `QUIVER_COORDINATOR_ADDRESS`/`PRIVATE_KEY` mismatch, or wrong network | | Reveal reverts `IncorrectProviderRevelation` | `FLETCHER_SEED`/`FLETCHER_CHAIN_LENGTH`/`FLETCHER_ANCHOR` don't match what was registered | | `TooManyHashes` on request | provider is behind on reveals — Fletcher down or out of gas | | Verification fails on Blockscout | pass `--verifier blockscout` and the correct `--verifier-url …/api` | ============================================================================== # SOURCE: https://quiver.foundation/docs/fletcher-operations.md (Keeper operations) ============================================================================== # Operating Fletcher (the keeper) **Fletcher** is the provider's off-chain service. It holds the hash-chain `seed`, watches for callback requests addressed to your provider, computes the matching reveal, and submits `revealWithCallback` to deliver randomness. This guide covers running it reliably. --- ## What Fletcher does 1. **Preflight** — confirms the provider is registered and reports how many random values remain in the chain. 2. **Backfill** — scans recent blocks for callback requests missed while offline and queues them. 3. **Watch** — subscribes to `RandomnessRequested` for your provider and fulfills each callback request (serially, to avoid nonce races). 4. Skips requests already fulfilled on-chain; retries transient failures. It does **not** need to serve the pull flow — pull-flow requesters reveal themselves. If you also offer pull-flow randomness, expose an endpoint that returns `revelation ` for already-requested sequence numbers only (never future ones). ## CLI ``` fletcher genseed # generate a 32-byte seed (store as FLETCHER_SEED) fletcher register # register the provider (commitment = keccak^N(seed)) fletcher run # start the keeper fletcher info # print the on-chain provider record fletcher revelation # print the revealed value for a sequence (debug / pull-flow serving) fletcher rotate # rotate onto a fresh chain segment ``` Run via `pnpm --filter @quiver/fletcher exec tsx src/index.ts `, or build (`pnpm --filter @quiver/fletcher build`) and use the `fletcher` bin. ## Configuration (environment) | Var | Required | Meaning | | --- | --- | --- | | `FLETCHER_NETWORK` | yes | `rh_testnet` \| `rh_mainnet` \| `local` | | `RH_TESTNET_RPC_URL` / `RH_MAINNET_RPC_URL` / `LOCAL_RPC_URL` | recommended | RPC endpoint (use a dedicated provider in prod) | | `QUIVER_COORDINATOR_ADDRESS` | yes | deployed coordinator | | `PRIVATE_KEY` | yes | the provider/operator key | | `FLETCHER_SEED` | yes | 32-byte hash-chain seed — **secret** | | `FLETCHER_CHAIN_LENGTH` | yes | Random values per chain segment | | `FLETCHER_FEE_WEI` | no (0) | per-request fee at registration | | `FLETCHER_MAX_NUM_HASHES` | no (500) | reveal hash bound at registration | | `FLETCHER_ANCHOR` | no (0) | sequence anchor of the current segment (set after a rotation) | | `FLETCHER_BACKFILL_BLOCKS` | no (5000) | startup look-back window | | `FLETCHER_POLL_MS` | no (3000) | event poll interval | ## Seed security — this is the crown jewel Anyone who learns your `seed` can **predict every future value** and thus every outcome you'll serve. Treat it exactly like a private key: - Generate with `fletcher genseed` (CSPRNG). Never derive it from something guessable. - Store in a secrets manager / KMS, not in a plaintext file on a shared box. - Never log it, commit it, or put it in a shell history (`export` from a secrets tool). - **Rotate on any suspicion of compromise** (below). Future values become safe immediately. ## Rotation Chains are finite. Rotate before exhaustion (Fletcher warns when values run low) or to retire a compromised seed: ```bash NEW=$(fletcher genseed 2>/dev/null) fletcher rotate "$NEW" 100000 # then update env for the NEW segment and restart: # FLETCHER_SEED=$NEW FLETCHER_CHAIN_LENGTH=100000 FLETCHER_ANCHOR= ``` Rotation anchors the new chain at `sequenceNumber - 1`, so the next request verifies in one hash. **Keep the old segment's config** if old requests may still be outstanding — Fletcher must reveal those against the old seed. For zero-downtime multi-segment serving, run the keeper with a persisted list of segments (roadmap) or briefly run two instances (old-segment reveal + new-segment run). ## Running in production - **Process manager:** systemd or a container with `restart: always`. Fletcher exits cleanly on SIGINT/SIGTERM. - **Dedicated RPC:** public endpoints are rate-limited; use Alchemy or another dedicated provider for both reads (event polling) and writes. - **Operator ETH:** the provider key pays gas for every reveal. Alert and top up before it runs dry. (A dry keeper = withheld reveals = stalled consumers.) - **Monitoring / alerts:** - low operator ETH balance, - remaining values below a threshold (`endSeq - sequenceNumber`), - reveal failures / growing gap between requests and reveals, - `CallbackFailed` volume (consumer bugs, not yours, but worth visibility). - **Idempotency:** Fletcher tracks seen sequence numbers and re-checks on-chain state, so a restart won't double-submit. ## Example systemd unit ```ini [Unit] Description=Quiver Fletcher keeper After=network-online.target [Service] WorkingDirectory=/opt/quiver EnvironmentFile=/etc/quiver/fletcher.env # holds FLETCHER_SEED etc. (chmod 600) ExecStart=/usr/bin/node /opt/quiver/fletcher/dist/index.js run Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ## Failure handling reference | Situation | Fletcher behavior | | --- | --- | | Request already fulfilled | detected via `getRequest` → skipped | | Reveal tx fails (RPC/gas) | logs error, drops from "seen" so it retries | | Consumer callback reverts | on-chain buffer + `CallbackFailed`; consumer retries via `retryCallback` | | seq outside current segment | error asking you to configure the segment's seed/anchor | | Random values nearly exhausted | warns on preflight — rotate |