@d20dao/vrf-sdk 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -11
- package/API.md +411 -193
- package/BUILD-MANIFEST.json +36 -24
- package/CHANGELOG.md +53 -0
- package/PROTOCOL-PROVENANCE.json +15 -9
- package/README.md +66 -26
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/abi/D20BeaconVerifier.json +165 -0
- package/abi/EpochEntropy.json +233 -0
- package/dist/abi.d.ts +303 -0
- package/dist/abi.js +2 -1
- package/dist/beacon.d.ts +28 -0
- package/dist/beacon.js +133 -0
- package/dist/epoch.d.ts +11 -3
- package/dist/epoch.js +70 -9
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/sources.d.ts +11 -0
- package/dist/sources.js +72 -0
- package/notices/BLS-BN254-LICENSE +21 -0
- package/notices/PROVENANCE.md +12 -0
- package/package.json +30 -4
package/AGENTS.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# d20dao consumer-agent guide
|
|
2
2
|
|
|
3
|
-
Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.
|
|
3
|
+
Use this guide when integrating @d20dao/vrf-sdk into an application or interpreting its public evidence. Install with `npm install @d20dao/vrf-sdk` (0.5.0 or newer). The package provides a general randomness interface; the dice, raffle and loot-drop contracts under examples/ are the ones to copy. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used. The installed API.md (`@d20dao/vrf-sdk/API.md`, generated from the packaged ABIs) lists every D20VRFCoordinator, EpochEntropy and D20BeaconVerifier function, event and error with its selector, caller, the calls that raise each error and what to do about it.
|
|
4
4
|
|
|
5
5
|
Integration order: network parameters and coordinator proxy; install and compiler setup; consumer contract copied from an example (payment pattern, callback gas); off-chain quote and request through the consumer; wait for and read the result; expiry, refund and retry gas. https://github.com/d20dao/randomizer-demo is a complete dapp built this way (consumer with one function per randomness option, single-page UI, read-only RPC relay), running at https://mainnet-demo.d20dao.org.
|
|
6
6
|
|
|
7
7
|
## Best practices
|
|
8
8
|
|
|
9
|
-
Quote with quoteFeeAt(callbackGasLimit, the latest header baseFeePerGas) plus a buffer, never with quoteFee through eth_call, where the base fee reads as 0 and the quote collapses to minFee. Send requests from a contract, never an EOA. Keep the callback small: store the result and do the work later; a failed callback is rolled back and anyone can redeliver the same accepted word with retryCallback, with more gas if needed. Map each request id to your own context and reject unknown or already-finished callbacks. Derive many values from one word (diceRoll count, chooseMany, shuffle, or keccak256(abi.encode(word, i))) rather than sending several requests. Handle the 60-second expiry path and choose a refund address that can receive a native transfer or call withdrawRefundCredit. Never re-roll a result you dislike; once fulfilled is true the word is final. Never use blockhash or block.timestamp as randomness. Withdraw the refund credit a fee buffer leaves behind, or return the change in the requesting transaction. Freeze any list before requesting an index into it.
|
|
9
|
+
Quote with quoteFeeAt(callbackGasLimit, the latest header baseFeePerGas) plus a buffer, never with quoteFee through eth_call, where the base fee reads as 0 and the quote collapses to minFee. Send requests from a contract, never an EOA. Keep the callback small: store the result and do the work later; a failed callback is rolled back and anyone can redeliver the same accepted word with retryCallback, with more gas if needed. Map each request id to your own context and reject unknown or already-finished callbacks. Derive many values from one word (diceRoll count, chooseMany, shuffle, or keccak256(abi.encode(word, i))) rather than sending several requests. Handle the 60-second expiry path and choose a refund address that can receive a native transfer or call withdrawRefundCredit. Never re-roll a result you dislike; once fulfilled is true the word is final. Never use blockhash or block.timestamp as randomness. Withdraw the refund credit a fee buffer leaves behind, or return the change in the requesting transaction. Close bets and entries in the requesting transaction: the pending fulfillment reveals the word about one block before it lands, so nothing the result decides may change until the callback. Freeze any list before requesting an index into it.
|
|
10
10
|
|
|
11
11
|
## Networks and toolchain
|
|
12
12
|
|
|
13
|
-
Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Both networks run the
|
|
13
|
+
Arc Mainnet: chain 5042, live service, RPC https://rpc.mainnet.arc.io, explorer https://explorer.arc.io, D20DAO explorer https://arc.d20dao.org, manifest https://d20dao.org/deployments/arc-mainnet.json. Arc Testnet: chain 5042002, development, RPC https://rpc.testnet.arc.io, explorer https://testnet.arcscan.app, D20DAO explorer https://arc-testnet.d20dao.org, manifest https://d20dao.org/deployments/arc-testnet.json. Both networks run the coordinator implementation this package describes, behind unchanged proxy addresses. The registry implementation with beacon recipes, and the D20BeaconVerifier it calls, are live behind the Arc Testnet registry proxy since block 64712965 and behind the Arc Mainnet registry proxy since block 23724929 (transaction 0x5a7a2fa8f15eefccee99f6bd363ce7717e65c5571c8c76396337fd8a1261a7cb); take addresses and code hashes from the manifests. Native USDC (18 decimals) pays gas and request fees; test USDC comes from the faucet linked at https://docs.arc.io/arc/references/connect-to-arc. Use Node 22.13+, solc 0.8.28 and evmVersion cancun. Hardhat resolves `@d20dao/vrf-sdk/contracts/...` from node_modules; Foundry needs the remapping `@d20dao/vrf-sdk/=node_modules/@d20dao/vrf-sdk/`. Consumer sources need no OpenZeppelin. To add a network to a browser wallet, call wallet_addEthereumChain with chainId '0x13b2' (5042, chainName 'Arc Mainnet') or '0x4cef52' (5042002, 'Arc Testnet'), nativeCurrency { name: 'USDC', symbol: 'USDC', decimals: 18 }, and the rpcUrls and blockExplorerUrls above. Website guides: https://d20dao.org/docs, index https://d20dao.org/llms.txt. Protocol source: the SDK repository's protocol/ folder (https://github.com/d20dao/d20-sdk/tree/main/protocol), a byte-for-byte copy of the keeper source at the commit in PROTOCOL-PROVENANCE.json, checked by its SHA-256 list. The public keeper repository (https://github.com/d20dao/keeper) publishes release snapshots, and that commit is ahead of its latest release.
|
|
14
14
|
|
|
15
15
|
## Public interfaces
|
|
16
16
|
|
|
17
|
-
Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quoteRequestFee from @d20dao/vrf-sdk. The epoch helpers are also exported from the /epoch entry point, which alone exports MAX_ATTESTATION_AGE. Import coordinatorAbi and
|
|
17
|
+
Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quoteRequestFee from @d20dao/vrf-sdk. The epoch helpers are also exported from the /epoch entry point, which alone exports MAX_ATTESTATION_AGE. The root exports the beacon helpers: DRAND_EVMNET, BEACON_TEMPLATE, beaconRoundTime, beaconRoundAt, encodeBeaconRound, decodeBeaconRound, beaconCanonicalRequest, beaconSlotSigner, beaconRoundMessage, verifyBeaconRound and the BeaconRegistration type. Import coordinatorAbi, epochEntropyAbi and beaconVerifierAbi from /abi. Solidity consumers use D20VRFConsumer, ID20VRF, D20VRFRequests and RandomnessMapping under /contracts with compiler 0.8.28. ID20VRF exposes quoteFee(callbackGasLimit), quoteFeeAt(callbackGasLimit, baseFee), requestRandomness(clientSeed, callbackGasLimit, refundAddress), requestMappedRandomness(..., spec) and getMappedResult(requestId).
|
|
18
18
|
|
|
19
|
-
The package is ESM only, for Node and bundlers; bundle it (Vite, webpack, esbuild) for browsers. quoteRequestFee expects an ethers v6 provider (or getBlock/call with ethers-v6 shapes); with other clients read the latest header baseFeePerGas, add a buffer and call quoteFeeAt. Never quote with quoteFee through eth_call, which reports a base fee of 0.
|
|
19
|
+
The package is ESM only, for Node and bundlers; bundle it (Vite, webpack, esbuild) for browsers. The BN254 curve behind beacon verification (about 45 ms to evaluate) loads when the package is imported in Node; a bundler leaves it out of a bundle that reaches no verification (verifyBeaconRound, beaconRoundMessage, verifyEpochAttestation, replayEpochCommitment, replayCoordinator). quoteRequestFee expects an ethers v6 provider (or getBlock/call with ethers-v6 shapes); with other clients read the latest header baseFeePerGas, add a buffer and call quoteFeeAt. Never quote with quoteFee through eth_call, which reports a base fee of 0.
|
|
20
20
|
|
|
21
21
|
Names that are easy to confuse: refundBps() is the current refund ratio copied into each new request, requestRefundBps(requestId) the ratio one request copied and is refunded at; pricing() and quoteFee price future requests, requestFeePaid(requestId) is what one request escrowed; keeperFeeBps() has no per-request copy and is read at proof acceptance. Solidity RandomnessMapping.Spec and the TypeScript MappingSpec returned by builtins carry the same fields (operation, lower, upper, count, population; lower and upper are bigint in TypeScript), ethers encodes a MappingSpec for the struct unchanged and hashMapping(spec) equals the request's mappingHash.
|
|
22
22
|
|
|
@@ -40,7 +40,7 @@ Invalid specs revert with InvalidMapping (throw in TypeScript). Callbacks always
|
|
|
40
40
|
|
|
41
41
|
## Pricing and payment
|
|
42
42
|
|
|
43
|
-
fee = max(minFee, feeMultiplier × baseFee × (fulfillGasOverhead + callbackGasLimit)), evaluated with the base fee of the requesting transaction. pricing() returns the live (minFee, feeMultiplier, fulfillGasOverhead); the owner may change them within bounds (minFee at most 10 USDC in 18-decimal native units, multiplier 0–20 where 0 is a flat minFee, overhead 100,000–2,000,000 gas) and emits PricingChanged. Both Arc deployments were initialized with a 0.08 USDC minimum fee, multiplier 5, overhead 300,000 gas and a 50% keeper share (keeperFeeBps 5000); these are initialization values, not fixed prices. Examples at
|
|
43
|
+
fee = max(minFee, feeMultiplier × baseFee × (fulfillGasOverhead + callbackGasLimit)), evaluated with the base fee of the requesting transaction. pricing() returns the live (minFee, feeMultiplier, fulfillGasOverhead); the owner may change them within bounds (minFee at most 10 USDC in 18-decimal native units, multiplier 0–20 where 0 is a flat minFee, overhead 100,000–2,000,000 gas) and emits PricingChanged. Both Arc deployments were initialized with a 0.08 USDC minimum fee, multiplier 5, overhead 300,000 gas and a 50% keeper share (keeperFeeBps 5000); these are initialization values, not fixed prices. Arc Testnet still uses them. Since 2026-09-18 Arc Mainnet charges a 0.02 USDC minimum fee, multiplier 3, overhead 300,000 gas and a 60% keeper share (keeperFeeBps 6000). Examples at the initialization values: at 176 gwei with 100,000 callback gas the fee is 5 × 176 gwei × 400,000 = 0.352 USDC; at 20 gwei the dynamic part is 0.04 USDC, so the 0.08 USDC minimum applies. At Arc Mainnet pricing and 20 gwei, 100,000 callback gas costs 3 × 20 gwei × 400,000 = 0.024 USDC. Read live values; never hard-code a price.
|
|
44
44
|
|
|
45
45
|
Send msg.value >= fee. Less reverts with IncorrectFee(expected, actual). Exactly the quote is escrowed (requestFeePaid, emitted as feePaid in RandomnessRequested); any excess is credited to the refund address as refund credit (FeeOverpaymentCredited, refundCredits) and only that address can pull it with withdrawRefundCredit(recipient). Choose a refund address that can call withdrawRefundCredit or receive a plain native transfer.
|
|
46
46
|
|
|
@@ -52,7 +52,7 @@ clientSeed need not be unique or secret: the VRF seed also binds chain, coordina
|
|
|
52
52
|
|
|
53
53
|
## Request lifecycle
|
|
54
54
|
|
|
55
|
-
Epochs last 200 blocks. The keeper prepares the first
|
|
55
|
+
Epochs last 200 blocks. The keeper prepares the first valid snapshot locally using the source anchor at epochStart-1: a signed API record, or for a beacon recipe a drand round. A saved signed record is never refreshed; a saved drand round too old to be accepted is replaced by a current one while no commit of its epoch was sent. A catalog of one source, such as the drand catalog, has no fallback. Idle preparation causes no publication transaction. Unused snapshots may remain locally for 50 epochs/10,000 blocks, with live-request and unresolved-transaction protection.
|
|
56
56
|
|
|
57
57
|
A request escrows its quoted fee even if its epoch is unpublished and fixes its request block, epoch, client seed, mapping, refund address, feePaid, refundBps and 60-second deadline. Live paid demand triggers publication of the saved packet. The target becomes max(requestBlock, committedBlock+1); no usable VRF seed exists until that future hash is known. Older-epoch demand can settle across a boundary without changing its packet.
|
|
58
58
|
|
|
@@ -70,13 +70,13 @@ Keepers may fulfill up to 16 requests in one fulfillRandomnessBatch transaction.
|
|
|
70
70
|
|
|
71
71
|
## Verification and trust
|
|
72
72
|
|
|
73
|
-
Epoch sources are recipes in an owner-managed, append-only registry: a canonical request (its keccak256 is the signed query hash), a data template fixing the exact signed bytes and the gateway request
|
|
73
|
+
Epoch sources are recipes in an owner-managed, append-only registry: a canonical request (its keccak256 is the signed query hash), a data template fixing the exact signed bytes and the body keepers send (the gateway request JSON, or a beacon's canonical request). registerRecipe appends the next id (0–255, event RecipeRegistered with the full definition) and registerBeacon appends a beacon recipe (also BeaconRegistered); getRecipe(id), beaconOf(id) and recipeCount() read them; a recipe never changes. Built-in recipes: 0 Hyperliquid BTC daily notional volume, 1 dRPC Ethereum block hash, 2 TickerLayer BTCUSD last trade, 3 TickerLayer ETHUSD last trade, 4 Nodary ETH/USD, 5 dRPC Base block hash (BUILTIN_EPOCH_RECIPES). Recipes 6–10 are the passthrough form of built-in recipes 0, 1, 2, 4 and 5 (the same signed records under other request hashes; passthroughEpochRecipe(builtinId) builds a definition). Recipe 11 is the drand evmnet beacon. A catalog is 1–10 ordered sources, each a registered recipe with its signer; the initial catalog is recipes 0–3, and the owner schedules others. Catalogs in force: Arc Testnet [0,1,2,4,5] from epoch 966 (2026-09-17), [6,7,8,9,10] from epoch 10108 (2026-09-28) and [11] from epoch 11319 (2026-09-30); Arc Mainnet [0,1,2,4,5] from epoch 848 (2026-09-18), [6,7,8,9,10] from epoch 10070 (2026-09-28) and [11] from epoch 12448 (2026-10-01). Read catalogAt(epochId) for the catalog an epoch actually used. The epoch anchor selects one source; fallback attempt n (1 to count−1) is the source n positions later, valid only from n × 20 blocks into the epoch (sourceCountAt, getEpochFallbackSelection, fallbackOpensAt, commitEpochFallback). replayEpochCommitment derives the attempt from the committed source and rejects a commit block before its window. Signed data must match the recipe's data template exactly (matchesDataTemplate; opcodes LITERAL 0x01, HEX 0x02, DECIMAL 0x03, INTEGER 0x04; README Data templates) and is at most 128 bytes; preserve the entire exact signed response. Signatures establish wrapper provenance, not unbiased upstream data. A beacon epoch commits one drand round: its number in decimal as the data, its scheduled time genesis + (round − 1) × period as the timestamp and the beacon's 64-byte BLS signature, which the verifier of the recipe's registration checks within BEACON_VERIFY_GAS (BeaconGasTooLow when the transaction's gas cannot give it that); a catalog's signer for a beacon slot is slotSigner(recipe). A drand signature establishes what the beacon's key signed for that round, not that the beacon is unbiased; the committer picks which round scheduled within the last 240 seconds to publish, and one round can serve several consecutive epochs, so key an epoch on its epoch ID or epoch hash, never on its round or data hash. The registry does not check that the chain hash names the key's network or that the verifier is the reviewed D20BeaconVerifier, and replay does not call the verifier: check its address and runtime code against the manifest. At publication an attestation (a signed record, or a round at its scheduled time) may be at most 240 seconds old (MAX_ATTESTATION_AGE) and never future-dated.
|
|
74
74
|
|
|
75
|
-
Catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(recipes, signers, fromEpoch) at least two epochs ahead (event CatalogScheduled(fromEpoch, catalogHash, recipes, signers)). A new schedule replaces a version that
|
|
75
|
+
Catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(recipes, signers, fromEpoch) at least two epochs ahead (event CatalogScheduled(fromEpoch, catalogHash, recipes, signers)). A new schedule replaces a pending version, one that takes effect two or more epochs ahead; the version that takes effect at the next epoch is kept, as is every active one, so the current and next epoch, prepared snapshots and open requests keep their catalog. A beacon recipe is listed with slotSigner(recipe) as its signer. Replay must use the epoch's catalogAt(epochId) view, turned into epoch.catalog with resolveEpochCatalog, or the CatalogScheduled history without replaced versions; replayEpochCommitment binds it to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash. Replay uses BUILTIN_EPOCH_RECIPES unless epoch.catalog.recipeBook supplies definitions; for other recipes read them with readEpochRecipes (getRecipe, query hash checked, plus beaconOf for a beacon recipe so that the recipe book carries its registration; an optional { blockTag } reads as of a block, for a registry whose implementation no longer has beaconOf). Epochs under recipes 0–5 replay with the built-in definitions; recipes 6–11 need their registered definitions. replayEpochCommitment and replayCoordinator verify both record types: a signed record against the catalog's signer, a beacon round against its registration (round, scheduled time, signature, slotSigner). Always confirm the implementations behind both proxies against the manifest.
|
|
76
76
|
|
|
77
|
-
Decode
|
|
77
|
+
Decode the registry's EpochCommitted packet with decodeEpochEvidencePacket and the coordinator's FulfillmentEvidence with decodeEvidencePacket, each in its trusted registry or coordinator event context. Proof evidence is 416 bytes; a single fulfillRandomness call is 452 calldata bytes. Supply independently trusted successful receipts, proxy implementation history, source/publication/request/target blocks and timestamps, initialized key/configuration and the original packets (a signed record or a beacon round). Compare both event and stored transcript commitments. Decoding and mapping alone do not verify origin; replay does not authenticate RPC or establish inclusion.
|
|
78
78
|
|
|
79
|
-
Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, allow up to four backup committers (they publish epochs under the committer's rules and earn the keeper share of the requests they serve themselves), register recipes, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Check it when you integrate and again whenever the deployment manifest records an upgrade or a proxy emits Upgraded(implementation); a mismatch with the manifest means stop and review before sending more requests.
|
|
79
|
+
Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership; renounceOwnership reverts on both. Implementations are locked against initialization. The owner can rotate committer and fee recipient, allow up to four backup committers (they publish epochs under the committer's rules and earn the keeper share of the requests they serve themselves), register recipes and beacons, adjust the keeper share, tune bounded pricing, lower the refund ratio for future requests and schedule future catalogs; no setter rewrites a request, a published epoch or the VRF key. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Check it when you integrate and again whenever the deployment manifest records an upgrade or a proxy emits Upgraded(implementation); a mismatch with the manifest means stop and review before sending more requests.
|
|
80
80
|
|
|
81
81
|
The contracts have not had an external security audit; the coordinator's "Prototype: not audited or validated on Arc" source comment is unchanged because the source is part of deployed bytecode metadata, and the service is live on Arc Mainnet. On Arc Mainnet the owner of both proxies is the DAO treasury Safe 0xB57f656149749eff6b496dF090336491f977E744. The VRF key holder can withhold a proof, which leads to a refund, but cannot substitute another result for a fixed seed.
|
|
82
82
|
|