@d20dao/vrf-sdk 0.1.2 → 0.3.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 CHANGED
@@ -1,43 +1,53 @@
1
- # d20dao consumer-agent guide
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`. The package provides a general randomness interface; dice and mining contracts are examples. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used.
4
-
5
- ## Public interfaces
6
-
7
- Import builtins, mapRandomness, decodeEvidencePacket and replayCoordinator from @d20dao/vrf-sdk. Epoch helpers also have an /epoch entrypoint. Import coordinatorAbi and epochEntropyAbi from /abi. Solidity consumers use D20VRFConsumer, ID20VRF, D20VRFRequests and RandomnessMapping under /contracts with compiler 0.8.28.
8
-
9
- RequestContext binds chainId, effective coordinator proxy, keyHash, requestId, consumer, clientSeed, mapping, requestBlock, targetBlock, blockHash, epochId and epochHash. Consult Parameters<typeof replayCoordinator>[0] for the complete trusted replay input. configuration.feeRecipient uses the initialized initialFeeRecipient, not the current payout address.
10
-
11
- ## Request lifecycle
12
-
13
- Epochs last 200 blocks. The keeper prepares the first validated API3 snapshot locally using the source anchor at epochStart-1. Idle preparation causes no publication transaction. Unused snapshots may remain locally for 50 epochs/10,000 blocks, with live-request and unresolved-transaction protection.
14
-
15
- After activation, a consumer escrows the exact requestFee even if its epoch is unpublished. Live paid demand triggers publication of that saved packet. The target becomes max(requestBlock,committedBlock+1); no usable VRF seed exists until that future hash is known. Preserve original request block, epoch, client seed, mapping, recipient and 60-second deadline. Older-epoch demand can settle across a boundary without changing its packet.
16
-
17
- Require exact payment and keep caller/request association stable. D20VRFConsumer authenticates the coordinator proxy; verify the expected request and store the raw callback word with minimal work. Mapped requests still callback with bytes32; use getMappedResult or canonical mapping. Keep application actions and payments separate from the callback.
18
-
19
- Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. Callback failure still earns service payment; retryCallback redelivers only the same accepted result. After expiry, an unfulfilled request refunds its fixed recipient or refund credit. Application-payment refunds are separate.
20
-
21
- Keeper share pays the configured registry committer, not an arbitrary proof submitter. Failed payment creates keeper credit. Refund escrow is separate, and retrying delivery cannot pay a second share.
22
-
23
- ## Verification and trust
24
-
25
- The four ordered recipe slots are Hyperliquid BTC volume, ANU, TickerLayer BTCUSD and TickerLayer ETHUSD; the latter two share a provider signer. Preserve the entire exact signed response, limited to 128 bytes. Signatures establish wrapper provenance, not unbiased upstream data.
26
-
27
- Decode epoch evidence using its trusted registry/event context and decodeEvidencePacket for FulfillmentEvidence. Proof evidence is 416 bytes; fulfillment calldata is 452 bytes. Supply independently trusted successful receipts, proxy implementation history, source/publication/request/target blocks and timestamps, initialized key/configuration and original signed packets. Compare both event and stored transcript commitments. Decoding and mapping alone do not verify origin; replay does not authenticate RPC or establish inclusion.
28
-
29
- Both service contracts use atomically initialized D20Proxy endpoints with owner-authorized UUPS upgrades and two-step ownership. Implementations are locked against initialization. Upgrade authority is trusted. Verify the implementation history of BOTH coordinator and registry; stable proxy addresses alone do not identify executed code. Operator pins stop processing on unreviewed changes while preserving recovery data.
30
-
31
- ## Service boundaries
32
-
33
- Always configure the actual chain explicitly; there is no implicit Arc network default. Use the correct public deployment proxy and configuration before live requests. Healthy process status does not guarantee a particular request's timely fulfillment.
34
-
35
- This SDK holds no signer or bot keys, runs no keeper/prover and exposes no operator API. Optional Telegram access is disabled by default and limited to read-only /status and /keeper in the configured operator chat. Those commands cannot alter configuration or send transactions. Docker provisioning, upgrades, funding and publishing are separate operator actions, not consequences of SDK integration.
36
-
37
- ## Optional refund notification
38
-
39
- A coordinator with refund-hook support calls `onRefund(requestId)` on the original consumer after the fee is paid to its fixed refund address or recorded as backed credit. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
40
-
41
- The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas. Never request new randomness from within either callback; use a separate application transaction.
42
-
43
- This SDK includes the new source interface. Installing it does not upgrade a deployed coordinator: verify the implementation and its refund-hook capability before relying on notification delivery.
1
+ # d20dao consumer-agent guide
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`. The package provides a general randomness interface; dice and mining contracts are examples. Read installed declarations for exact types and match the packaged PROTOCOL-PROVENANCE.json to the deployment being used.
4
+
5
+ ## Public interfaces
6
+
7
+ Import builtins, mapRandomness, decodeEvidencePacket, replayCoordinator and quoteRequestFee from @d20dao/vrf-sdk. Epoch helpers and MAX_ATTESTATION_AGE also have an /epoch entrypoint. Import coordinatorAbi and epochEntropyAbi 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).
8
+
9
+ RequestContext binds chainId, effective coordinator proxy, keyHash, requestId, consumer, clientSeed, mapping, requestBlock, targetBlock, blockHash, epochId and epochHash. Consult Parameters<typeof replayCoordinator>[0] for the complete trusted replay input. configuration.feeRecipient uses the initialized initialFeeRecipient and configuration.initialMinFee the initialize fee argument (getter initialMinFee), not the live payout address or pricing.
10
+
11
+ ## Pricing and payment
12
+
13
+ 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. Initialization sets multiplier 5 and overhead 300,000; the deployment configuration sets a 0.08 USDC minimum and a 40% keeper share (keeperFeeBps 4000). Examples at those parameters: 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. Read live values; never hard-code a price.
14
+
15
+ 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.
16
+
17
+ A contract that requests in the same transaction pays quoteFee(callbackGasLimit), which is exact; D20VRFRequests helpers and MiningRandomnessConsumer do this from the contract balance. A wallet or backend must pay through a consumer contract (requests from EOAs revert) and must never quote quoteFee through eth_call: the base fee is commonly reported as 0 there (verified on Arc mainnet), the quote collapses to minFee and the transaction reverts. Quote with quoteFeeAt(callbackGasLimit, latestBlock.baseFeePerGas) plus a buffer and forward the whole amount. quoteRequestFee(provider, coordinator, callbackGasLimit, { bufferBps = 3000 }) does this with ethers 6: fee is the quote at the block's base fee, value is the quote at a base fee bufferBps higher (equal to fee when the minimum dominates); send value. The buffer covers base-fee movement until inclusion; the excess is refund credit, never revenue. On IncorrectFee, quote again and resend.
18
+
19
+ ## Request lifecycle
20
+
21
+ Epochs last 200 blocks. The keeper prepares the first validated API3 snapshot locally using the source anchor at epochStart-1. Idle preparation causes no publication transaction. Unused snapshots may remain locally for 50 epochs/10,000 blocks, with live-request and unresolved-transaction protection.
22
+
23
+ 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.
24
+
25
+ D20VRFConsumer authenticates the coordinator proxy; verify the expected request and store the raw callback word with minimal work. Mapped requests still callback with bytes32; use getMappedResult or canonical mapping. Keep application actions and payments separate from the callback.
26
+
27
+ Valid onchain acceptance at or before requestedAt+60 seconds is timely. A pending transaction is not acceptance. At acceptance keeperFeeBps of feePaid goes to the configured registry committer, not the proof submitter (a failed transfer becomes keeper credit), and the remainder becomes protocol fees. Callback failure still earns the fee; retryCallback redelivers only the same accepted result and cannot pay a second share.
28
+
29
+ After the deadline, anyone may call refundRequest(requestId). It pays feePaid × requestRefundBps / 10000 using the ratio snapshotted at request time (default 100%; the owner may lower it to no less than 50% for future requests only, event RefundBpsChanged) to the fixed refund address, or records it as that address's refund credit if the 30,000-gas transfer fails; the remainder is retained as protocol fees. Gas and application payments are separate.
30
+
31
+ Keepers may fulfill up to 16 requests in one fulfillRandomnessBatch transaction. Each served request emits the same per-request events and evidence as a single fulfillment and settles from its own feePaid; members already fulfilled, refunded or past their deadline emit FulfillmentSkipped(requestId, reason) with reason 1, 2 or 3, and any proof or readiness failure reverts the batch. Consumers see no difference. Indexers must rely on per-request events, not transaction calldata.
32
+
33
+ ## Verification and trust
34
+
35
+ The four ordered recipe slots are Hyperliquid BTC volume, ANU, TickerLayer BTCUSD and TickerLayer ETHUSD; the latter two share a provider signer. The epoch anchor selects one slot; fallback attempt n (1–3) is the slot n positions later, valid only from n × 20 blocks into the epoch (getEpochFallbackSelection, fallbackOpensAt, commitEpochFallback). replayEpochCommitment derives the attempt from the committed source and rejects a commit block before its window. Preserve the entire exact signed response, limited to 128 bytes. Signatures establish wrapper provenance, not unbiased upstream data. At publication an attestation may be at most 240 seconds old (MAX_ATTESTATION_AGE) and never future-dated.
36
+
37
+ Signer catalogs are per epoch. The registry owner can schedule a replacement with scheduleCatalog(signers, fromEpoch) at least two epochs ahead (event CatalogScheduled); the current and next epoch, prepared snapshots and open requests keep their signers. Replay must use signersAt(epochId) or the CatalogScheduled history as epoch.catalog.signers, which replayEpochCommitment binds to the record's catalogHash, while configuration.catalogHash stays the initial catalogHash() bound into protocolConfigurationHash.
38
+
39
+ Decode epoch evidence using its trusted registry/event context and decodeEvidencePacket for FulfillmentEvidence. 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 original signed packets. Compare both event and stored transcript commitments. Decoding and mapping alone do not verify origin; replay does not authenticate RPC or establish inclusion.
40
+
41
+ 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, 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. Operator pins stop processing on unreviewed changes while preserving recovery data.
42
+
43
+ ## Service boundaries
44
+
45
+ Always configure the actual chain explicitly; there is no implicit Arc network default. Take proxy addresses and code hashes from the keeper's deployment manifest for that chain and confirm the coordinator implementation exposes quoteFee/quoteFeeAt before live requests. Healthy process status does not guarantee a particular request's timely fulfillment.
46
+
47
+ This SDK holds no signer or bot keys, runs no keeper/prover and exposes no operator API. Optional Telegram access is disabled by default and limited to read-only /status and /keeper in the configured operator chat. Those commands cannot alter configuration or send transactions. Docker provisioning, upgrades, funding and publishing are separate operator actions, not consequences of SDK integration.
48
+
49
+ ## Optional refund notification
50
+
51
+ After refundRequest has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
52
+
53
+ The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas. Never request new randomness from within either callback; use a separate application transaction.
@@ -2,15 +2,15 @@
2
2
  "compiler": "0.8.28+commit.7893614a.Emscripten.clang",
3
3
  "protocol": {
4
4
  "sourceRepository": "https://github.com/d20dao/keeper",
5
- "sourceCommit": "d7e785dda57499220bd37d73bc6fad9226872dcc",
5
+ "sourceCommit": "640b60cb992a7e3add1efe8e7b392341732ea004",
6
6
  "note": "Current public d20dao protocol copied byte-for-byte from committed Git blobs. Operator backends, keys, test fixtures and proof generation are excluded from the package.",
7
7
  "files": {
8
8
  "LICENSE": "bfdc94eef4cfbec12a9ed47bb406c8125cf4c306facf7b8d92db58f133b1a703",
9
9
  "contracts/D20VRFConsumer.sol": "9200fcd8825756ba6978e42d94b52e82e6dea9b1ba35dc506652490194e16162",
10
- "contracts/D20VRFCoordinator.sol": "4f9f842e6b5cb024f4113af566172a0ef62973644ef91aae48d0caf48c301afa",
11
- "contracts/examples/MiningRandomnessConsumer.sol": "0a5d573266dbd3e531464cb795901e0f0060b9f6fbf9c7a52853f722093eb6a5",
12
- "contracts/interfaces/ID20VRF.sol": "a9897086d751339b237e11c70bf99034080c75e5cf7876f2c9147e09d61f48fb",
13
- "contracts/libraries/D20VRFRequests.sol": "8f0ee77eefd45d0667f9cd67f2db149850cd1f0da3bf5133335f8323cd5ea6d4",
10
+ "contracts/D20VRFCoordinator.sol": "f2c17545fe39323a39aef8a4e642965f5a607ba547365da0b542b5277be4dd2c",
11
+ "contracts/examples/MiningRandomnessConsumer.sol": "2f5c19d05e66bea313fff65269650fe1e3bea70bd6aa80fa3e3de8bee8c24649",
12
+ "contracts/interfaces/ID20VRF.sol": "0361aee07644cbf7c2bcde8ffd9188825d5be633c4a1520169a7702a38c1bdbc",
13
+ "contracts/libraries/D20VRFRequests.sol": "f87e66f7eb0350d9b8740f120b97085957793b826b81a0c7c80d58a3fcd92b76",
14
14
  "contracts/libraries/RandomnessMapping.sol": "00bce68c1a6d89903d72d1c3f9ee5e1a56392188ed29b6aa32ab7451ef180c91",
15
15
  "contracts/vendor/CHAINLINK-LICENSE": "25a502f8fd39602de8a98cc4835a4bf81851b095d6f2c5c130bf0863afac6c99",
16
16
  "contracts/vendor/PROVENANCE.md": "e6da392378851c47574ef808c9c7e17dea50054ffd0cae7a9070f76f8eed60f2",
@@ -21,12 +21,12 @@
21
21
  "src/replay.ts": "c97a5081e785e6a7f27987e2d4c4d9861fe8fa8cbc775b0152e337141d3d48d7",
22
22
  "src/sources.ts": "d8566e7809c67dc51acab3b58fc93d734d098c2eabbaf3657ac910affe12e31e",
23
23
  "src/verification.ts": "754e39554c0c432cf1404741503f66b4199b84f39003d8e42f300a7256ffaa6a",
24
- "src/epoch.ts": "2e49f7906d55d9edc144ae4ee142608d5629bc68d6596d0f4f424ebaa62d823a",
25
- "contracts/EpochEntropy.sol": "23fd4d040a6f3800a6edf8f9ace1c251f47975728b9a86d7874cd0bc7abe9972",
24
+ "src/epoch.ts": "41a3d60f063deccc6d19761dc63702a2fdd1efdc46f775b7202241c3eb4d1334",
25
+ "contracts/EpochEntropy.sol": "bdd4f9e5dab6626572a72a0c15e73b4f3f19c6933b7a122fad237a9784fa3cbb",
26
26
  "contracts/D20Proxy.sol": "83dcef3b72e2a0a4809c2d8df0d083d2a0f659fe2b3b01fadb1b2eaf8afb7286"
27
27
  }
28
28
  },
29
- "packageLockSha256": "1040ab4287e77f250a4ea54fcb68f5ed3d29b61582c5e52d2f7d1e05136a4529",
29
+ "packageLockSha256": "1607f850da1f95de8e49e64f61a3e5bab1b54ba3415678241403d6f5c66b6bf6",
30
30
  "buildDependencies": {
31
31
  "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol": "41a3040398d53999dea3251ff8906e11ec1a699362a1d8f4a55bfc7709cc00f3",
32
32
  "@openzeppelin/contracts/utils/ReentrancyGuard.sol": "94e409e8f6e3184236651a6cc2b1a6a3ea0f0a25eb85b71e524ad5791bb2fbc8",
@@ -56,10 +56,10 @@
56
56
  "sources": {
57
57
  "LICENSE": "bfdc94eef4cfbec12a9ed47bb406c8125cf4c306facf7b8d92db58f133b1a703",
58
58
  "contracts/D20VRFConsumer.sol": "9200fcd8825756ba6978e42d94b52e82e6dea9b1ba35dc506652490194e16162",
59
- "contracts/D20VRFCoordinator.sol": "4f9f842e6b5cb024f4113af566172a0ef62973644ef91aae48d0caf48c301afa",
60
- "contracts/examples/MiningRandomnessConsumer.sol": "0a5d573266dbd3e531464cb795901e0f0060b9f6fbf9c7a52853f722093eb6a5",
61
- "contracts/interfaces/ID20VRF.sol": "a9897086d751339b237e11c70bf99034080c75e5cf7876f2c9147e09d61f48fb",
62
- "contracts/libraries/D20VRFRequests.sol": "8f0ee77eefd45d0667f9cd67f2db149850cd1f0da3bf5133335f8323cd5ea6d4",
59
+ "contracts/D20VRFCoordinator.sol": "f2c17545fe39323a39aef8a4e642965f5a607ba547365da0b542b5277be4dd2c",
60
+ "contracts/examples/MiningRandomnessConsumer.sol": "2f5c19d05e66bea313fff65269650fe1e3bea70bd6aa80fa3e3de8bee8c24649",
61
+ "contracts/interfaces/ID20VRF.sol": "0361aee07644cbf7c2bcde8ffd9188825d5be633c4a1520169a7702a38c1bdbc",
62
+ "contracts/libraries/D20VRFRequests.sol": "f87e66f7eb0350d9b8740f120b97085957793b826b81a0c7c80d58a3fcd92b76",
63
63
  "contracts/libraries/RandomnessMapping.sol": "00bce68c1a6d89903d72d1c3f9ee5e1a56392188ed29b6aa32ab7451ef180c91",
64
64
  "contracts/vendor/CHAINLINK-LICENSE": "25a502f8fd39602de8a98cc4835a4bf81851b095d6f2c5c130bf0863afac6c99",
65
65
  "contracts/vendor/PROVENANCE.md": "e6da392378851c47574ef808c9c7e17dea50054ffd0cae7a9070f76f8eed60f2",
@@ -70,8 +70,11 @@
70
70
  "src/replay.ts": "c97a5081e785e6a7f27987e2d4c4d9861fe8fa8cbc775b0152e337141d3d48d7",
71
71
  "src/sources.ts": "d8566e7809c67dc51acab3b58fc93d734d098c2eabbaf3657ac910affe12e31e",
72
72
  "src/verification.ts": "754e39554c0c432cf1404741503f66b4199b84f39003d8e42f300a7256ffaa6a",
73
- "src/epoch.ts": "2e49f7906d55d9edc144ae4ee142608d5629bc68d6596d0f4f424ebaa62d823a",
74
- "contracts/EpochEntropy.sol": "23fd4d040a6f3800a6edf8f9ace1c251f47975728b9a86d7874cd0bc7abe9972",
73
+ "src/epoch.ts": "41a3d60f063deccc6d19761dc63702a2fdd1efdc46f775b7202241c3eb4d1334",
74
+ "contracts/EpochEntropy.sol": "bdd4f9e5dab6626572a72a0c15e73b4f3f19c6933b7a122fad237a9784fa3cbb",
75
75
  "contracts/D20Proxy.sol": "83dcef3b72e2a0a4809c2d8df0d083d2a0f659fe2b3b01fadb1b2eaf8afb7286"
76
+ },
77
+ "packageSources": {
78
+ "src/fees.ts": "836f6a6e14d638876a4ceca8f3369577edb5d242096b420606fb69c74bb7dcae"
76
79
  }
77
80
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "sourceRepository": "https://github.com/d20dao/keeper",
3
- "sourceCommit": "d7e785dda57499220bd37d73bc6fad9226872dcc",
3
+ "sourceCommit": "640b60cb992a7e3add1efe8e7b392341732ea004",
4
4
  "note": "Current public d20dao protocol copied byte-for-byte from committed Git blobs. Operator backends, keys, test fixtures and proof generation are excluded from the package.",
5
5
  "files": {
6
6
  "LICENSE": "bfdc94eef4cfbec12a9ed47bb406c8125cf4c306facf7b8d92db58f133b1a703",
7
7
  "contracts/D20VRFConsumer.sol": "9200fcd8825756ba6978e42d94b52e82e6dea9b1ba35dc506652490194e16162",
8
- "contracts/D20VRFCoordinator.sol": "4f9f842e6b5cb024f4113af566172a0ef62973644ef91aae48d0caf48c301afa",
9
- "contracts/examples/MiningRandomnessConsumer.sol": "0a5d573266dbd3e531464cb795901e0f0060b9f6fbf9c7a52853f722093eb6a5",
10
- "contracts/interfaces/ID20VRF.sol": "a9897086d751339b237e11c70bf99034080c75e5cf7876f2c9147e09d61f48fb",
11
- "contracts/libraries/D20VRFRequests.sol": "8f0ee77eefd45d0667f9cd67f2db149850cd1f0da3bf5133335f8323cd5ea6d4",
8
+ "contracts/D20VRFCoordinator.sol": "f2c17545fe39323a39aef8a4e642965f5a607ba547365da0b542b5277be4dd2c",
9
+ "contracts/examples/MiningRandomnessConsumer.sol": "2f5c19d05e66bea313fff65269650fe1e3bea70bd6aa80fa3e3de8bee8c24649",
10
+ "contracts/interfaces/ID20VRF.sol": "0361aee07644cbf7c2bcde8ffd9188825d5be633c4a1520169a7702a38c1bdbc",
11
+ "contracts/libraries/D20VRFRequests.sol": "f87e66f7eb0350d9b8740f120b97085957793b826b81a0c7c80d58a3fcd92b76",
12
12
  "contracts/libraries/RandomnessMapping.sol": "00bce68c1a6d89903d72d1c3f9ee5e1a56392188ed29b6aa32ab7451ef180c91",
13
13
  "contracts/vendor/CHAINLINK-LICENSE": "25a502f8fd39602de8a98cc4835a4bf81851b095d6f2c5c130bf0863afac6c99",
14
14
  "contracts/vendor/PROVENANCE.md": "e6da392378851c47574ef808c9c7e17dea50054ffd0cae7a9070f76f8eed60f2",
@@ -19,8 +19,8 @@
19
19
  "src/replay.ts": "c97a5081e785e6a7f27987e2d4c4d9861fe8fa8cbc775b0152e337141d3d48d7",
20
20
  "src/sources.ts": "d8566e7809c67dc51acab3b58fc93d734d098c2eabbaf3657ac910affe12e31e",
21
21
  "src/verification.ts": "754e39554c0c432cf1404741503f66b4199b84f39003d8e42f300a7256ffaa6a",
22
- "src/epoch.ts": "2e49f7906d55d9edc144ae4ee142608d5629bc68d6596d0f4f424ebaa62d823a",
23
- "contracts/EpochEntropy.sol": "23fd4d040a6f3800a6edf8f9ace1c251f47975728b9a86d7874cd0bc7abe9972",
22
+ "src/epoch.ts": "41a3d60f063deccc6d19761dc63702a2fdd1efdc46f775b7202241c3eb4d1334",
23
+ "contracts/EpochEntropy.sol": "bdd4f9e5dab6626572a72a0c15e73b4f3f19c6933b7a122fad237a9784fa3cbb",
24
24
  "contracts/D20Proxy.sol": "83dcef3b72e2a0a4809c2d8df0d083d2a0f659fe2b3b01fadb1b2eaf8afb7286"
25
25
  }
26
26
  }
package/README.md CHANGED
@@ -1,99 +1,151 @@
1
- # d20dao VRF SDK
2
-
3
- Public replay, mapping, epoch evidence and Solidity consumer helpers for a general randomness service. Package: `@d20dao/vrf-sdk` `0.1.2`.
4
-
5
- ## Getting started
6
-
7
- ```sh
8
- npm install @d20dao/vrf-sdk
9
- ```
10
-
11
- Use Node 22.13 or newer and Solidity 0.8.28. Configure the coordinator proxy from the [current deployment manifest](https://github.com/d20dao/keeper/blob/main/deployments/arc-testnet.json), then follow the consumer example below. Arc Testnet is chain 5042002; any consumer contract can request randomness with the current exact fee, without allowlisting. Read `requestFee()` at runtime and keep application payments separate.
12
-
13
- For agent-assisted integration, give your agent the installed `AGENTS.md` and `PROTOCOL-PROVENANCE.json`, plus the [integration skills](https://github.com/d20dao/skills). Website guides include Getting started, Copy prompt, `/llms.txt`, `/llms-full.txt` and `/agents.md`.
14
-
15
- ## Current request flow
16
-
17
- Epochs last 200 blocks. The keeper selects one of four fixed recipes using the canonical block hash at epoch start minus one and prepares its first validated API3 snapshot locally. Idle preparation publishes no transaction. An unused local snapshot can be retained for 50 epochs (10,000 blocks), subject to live-demand and unresolved-transaction protection.
18
-
19
- After activation, a consumer escrows the exact request fee even when the epoch packet is not published. The request fixes its original block, epoch, client seed, mapping, recipient and 60-second deadline. The keeper publishes the saved packet only for live paid demand. The randomness target becomes `max(requestBlock, committedBlock + 1)`, so its hash is unknown at publication. Before publication the request has no usable target or VRF seed. Multiple requests share the packet, and timely requests can settle across epoch boundaries without changing their epoch.
20
-
21
- The four ordered recipe slots are Hyperliquid BTC volume, ANU quantum data, TickerLayer BTCUSD lastTrade and TickerLayer ETHUSD lastTrade. Both TickerLayer slots use the same provider signer and crypto asset class. `EpochSigners` is a readonly four-address tuple. The full exact signed data is limited to 128 bytes and emitted publicly; do not crop or replace it. A signature establishes provider-wrapper provenance, not unbiased upstream data or guaranteed availability.
22
-
23
- ## Use locally
24
-
25
- For SDK development, run `npm ci` and `npm test` from this repository. The test builds, packs and installs a real tarball in an isolated consumer. `npm pack` also produces an installable local artifact.
26
-
27
- ```js
28
- import { builtins, mapRandomness, replayCoordinator } from '@d20dao/vrf-sdk';
29
- import { coordinatorAbi, epochEntropyAbi } from '@d20dao/vrf-sdk/abi';
30
- const mapping = builtins.d20();
31
- // Use only an independently verified accepted word for real outcomes.
32
- ```
33
-
34
- The root exports ESM and TypeScript declarations; `/epoch` exports epoch helpers. `/abi` exports `coordinatorAbi` and `epochEntropyAbi`, with JSON forms `D20VRFCoordinator.json` and `EpochEntropy.json`. The service implementations have locked empty constructors and explicit initializers. Registry initialization takes `address[4]`; it is not a four-address constructor deployment.
35
-
36
- ## Integrate a consumer
37
-
38
- Solidity imports require compiler 0.8.28 and your compiler's npm resolver:
39
-
40
- - `@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol`
41
- - `@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol`
42
- - `@d20dao/vrf-sdk/contracts/libraries/D20VRFRequests.sol`
43
- - `@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol`
44
- - `@d20dao/vrf-sdk/contracts/examples/MiningRandomnessConsumer.sol`
45
-
46
- `examples/DiceConsumer.sol` is one concrete consumer example. It requires exact payment, fixes the player's refund recipient and stores the authenticated raw callback word. Its mapped result is 1 through 20. Mapped callbacks still carry raw bytes32. Keep application actions separate from callbacks; the example does not implement application-payment refunds, claim locking or minting.
47
-
48
- Configure the chain explicitly; there is no implicit Arc network default. Pin the effective coordinator proxy address, initialized configuration and implementation history of both service proxies. Any consumer contract may request by paying the current exact fee. A constructor code-length check, SDK installation or permissionless request acceptance does not guarantee service.
49
-
50
- Timely service requires actual onchain proof acceptance at or before original request time +60 seconds. Callback failure still earns the fee; retryCallback redelivers only the same accepted result. Expired unfulfilled requests refund their fixed recipient or receive refund credit. Application-payment refunds remain separate.
51
-
52
- ## Replay and upgrades
53
-
54
- Use independently trusted successful receipts and state. Decode the registry EpochCommitted packet with decodeEpochEvidencePacket and verify with replayEpochCommitment. Use the original source anchor, exact packet, commit block/time, ordered signers and registry identity. Decode the coordinator FulfillmentEvidence packet with decodeEvidencePacket, then call replayCoordinator with its actual exported input type.
55
-
56
- RequestContext binds both requestBlock and targetBlock. Validate the epoch from the original request block, reconstruct the target from the actual publication block, and compare the event and stored transcript. Proof evidence is 416 bytes; fulfillment calldata is 452 bytes. Neither evidence packet has a version prefix. Choose the decoder from trusted emitter/event context. Decoding and mapping alone are not proof verification; replay does not authenticate RPC or establish receipt inclusion.
57
-
58
- D20VRFCoordinator and EpochEntropy use atomically initialized ERC1967 proxies with owner-authorized UUPS upgrades and two-step ownership transfers. The registry owner can change the committer; the coordinator owner can change fee recipient and keeper share. Upgrade authority can change code and is an explicit trust assumption. Keep requests, balances, credits, epochs and public replay intact across reviewed storage-compatible upgrades.
59
-
60
- For replay, populate configuration.feeRecipient from the initialized initialFeeRecipient, not the current payout address. Use the effective proxy addresses in request and registry context. Operators pin the proxy code, initialized configuration, and BOTH implementation addresses/runtime hashes; the keeper fails closed on an unreviewed implementation change. Existing proof/nonce data must survive the review and restart.
61
-
62
- The keeper share pays the configured registry committer, not an arbitrary proof submitter. Failed transfers become keeper credit. Refund escrow remains protected; callback retries do not pay a second fee share.
63
-
64
- ## Operational and release boundary
65
-
66
- This SDK contains no keeper service, API fetching, proof generation, signer secrets or deployment automation. The canonical keeper has a Docker install wrapper that builds, provisions separately supplied key files and starts from reviewed configuration; inspect its platform-specific guide before use. No deployment or funding is authorized by SDK installation.
67
-
68
- Optional Telegram access is disabled unless a bot token and numeric operator chat are explicitly configured. Only that chat can use read-only /status and /keeper commands. Commands never modify configuration or send transactions; notifications are best-effort observations, not chain evidence. This package neither reads bot credentials nor contacts Telegram.
69
-
70
- Builds use reviewed protocol Git blobs and verify every SHA-256 in PROTOCOL-PROVENANCE.json. BUILD-MANIFEST.json records source, dependency-lock and imported OpenZeppelin hashes. The UUPS build uses OpenZeppelin contracts and contracts-upgradeable 5.6.1. Consumer source is copied exactly; service implementations, operator code, test fixtures and provers are excluded from the tarball.
71
-
72
- Fixture provenance distinguishes explicit CI signatures from actual API3 responses. Fixtures are not included in the package. The browser-target bundle is executed under Node, not an actual browser session; independently trusted chain context is still required for real verification.
73
-
74
- SDK installation provides consumer and verification tooling. Chain availability, provider quotas, upgrade administration and application settlement remain separate concerns. A healthy process alone does not guarantee a particular request's timely fulfillment.
75
-
76
- ## Arc Testnet pilot
77
-
78
- Chain ID: **5042002**. Use the **coordinator proxy** when constructing a consumer.
79
-
80
- | Contract | Role | Arc Testnet address |
81
- | --- | --- | --- |
82
- | D20VRFCoordinator | Consumer entry point / proxy | [`0xd20dA0fDa41f84FCfA3423ae9F96B15910587B4E`](https://testnet.arcscan.app/address/0xd20dA0fDa41f84FCfA3423ae9F96B15910587B4E) |
83
- | EpochEntropy | Epoch registry / proxy | [`0xd20Da04e4D6d97a762A5b56993d723AA7663F204`](https://testnet.arcscan.app/address/0xd20Da04e4D6d97a762A5b56993d723AA7663F204) |
84
- | D20CostClient | Restricted pilot consumer / proxy | [`0xD20Da0Ab4F5c258d579D18dC5a6e652266BB9a20`](https://testnet.arcscan.app/address/0xD20Da0Ab4F5c258d579D18dC5a6e652266BB9a20) |
85
- | D20VRFCoordinator | Implementation | [`0xd20Da05E6bb360edA09a6a360291AB6AD7AA0c58`](https://testnet.arcscan.app/address/0xd20Da05E6bb360edA09a6a360291AB6AD7AA0c58) |
86
- | EpochEntropy | Implementation | [`0xd20Da0028F2B65d8c8C8512C7029EE02F94E8BF2`](https://testnet.arcscan.app/address/0xd20Da0028F2B65d8c8C8512C7029EE02F94E8BF2) |
87
- | D20CostClient | Implementation | [`0xd20dA0Ec8d33fB04184CbC13942657bDC1f5Bbc0`](https://testnet.arcscan.app/address/0xd20dA0Ec8d33fB04184CbC13942657bDC1f5Bbc0) |
88
-
89
- Addresses are copied from the deployment manifest, including the coordinator upgrade at block 62310349. Explorer links identify addresses; they do not assert explorer source-code verification. Implementation addresses can change through owner-authorized upgrades. The pilot consumer is test tooling, not a shared application entry point.
90
-
91
- A public testnet service is deployed on chain 5042002. Obtain current proxy addresses and independently checked code hashes from the [keeper deployment manifest](https://github.com/d20dao/keeper/blob/main/deployments/arc-testnet.json). Any consumer contract can request service by paying the current exact fee; no allowlist is required. The manifest includes the activated refund-notification implementation. The [small-sample measurements](https://github.com/d20dao/keeper/blob/main/docs/benchmarks/arc-testnet-pilot-2026-09-15.json) cover proof acceptance, same-result callback repair and expired-request refunds; they are not an SLA.
92
-
93
- ## Optional refund notification
94
-
95
- A coordinator with refund-hook support calls `onRefund(requestId)` on the original consumer after the fee is paid to its fixed refund address or recorded as backed credit. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
96
-
97
- The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas. Never request new randomness from within either callback; use a separate application transaction.
98
-
99
- This SDK includes the new source interface. Installing it does not upgrade a deployed coordinator: verify the implementation and its refund-hook capability before relying on notification delivery.
1
+ # d20dao VRF SDK
2
+
3
+ Public replay, mapping, epoch evidence, off-chain fee quoting and Solidity consumer helpers for a general randomness service. Package: `@d20dao/vrf-sdk` `0.2.0`.
4
+
5
+ ## Getting started
6
+
7
+ ```sh
8
+ npm install @d20dao/vrf-sdk
9
+ ```
10
+
11
+ Use Node 22.13 or newer and Solidity 0.8.28. Configure the coordinator proxy explicitly from the keeper's deployment manifest for the chain you use (Arc Testnet is chain 5042002; see [Deployments](#deployments)); there is no implicit network default. Any consumer contract can request randomness by paying at least the fee quoted for its transaction, without allowlisting. Requests must come from a contract; a wallet or backend pays through its own consumer contract.
12
+
13
+ For agent-assisted integration, give your agent the installed `AGENTS.md` and `PROTOCOL-PROVENANCE.json`, plus the [integration skills](https://github.com/d20dao/skills). Website guides include Getting started, Copy prompt, `/llms.txt`, `/llms-full.txt` and `/agents.md`.
14
+
15
+ ## Pricing
16
+
17
+ The coordinator prices every request from the base fee of the transaction that creates it:
18
+
19
+ ```
20
+ fee = max(minFee, feeMultiplier × baseFee × (fulfillGasOverhead + callbackGasLimit))
21
+ ```
22
+
23
+ `pricing()` returns the live `(minFee, feeMultiplier, fulfillGasOverhead)`. The owner can move them with `setPricing(minFee, multiplier, overhead)` (event `PricingChanged`) only within fixed bounds: `minFee` at most 10 USDC (`10e18` wei; native USDC on Arc uses 18 decimals), `feeMultiplier` 0 to 20 where 0 means a flat `minFee`, `fulfillGasOverhead` 100,000 to 2,000,000 gas. Initialization sets multiplier 5 and overhead 300,000; the keeper's deployment configuration (`config/service.json`) sets a 0.08 USDC minimum fee and a 40% keeper share (`keeperFeeBps` 4000). Read the live values instead of hard-coding them; a pricing change never touches requests that are already open, because each request settles from the fee it escrowed.
24
+
25
+ Labelled examples with multiplier 5, overhead 300,000 and a 0.08 USDC minimum:
26
+
27
+ - **A, 176 gwei base fee, 100,000 callback gas.** Dynamic part 5 × 176 gwei × 400,000 = 0.352 USDC, above the minimum, so the fee is 0.352 USDC.
28
+ - **B, 20 gwei base fee, 100,000 callback gas.** Dynamic part 5 × 20 gwei × 400,000 = 0.04 USDC, below the minimum, so the fee is 0.08 USDC.
29
+ - **C, multiplier set to 0.** The fee is `minFee` at any base fee.
30
+
31
+ `quoteFeeAt(callbackGasLimit, baseFee)` evaluates the formula for a base fee you supply; `quoteFee(callbackGasLimit)` evaluates it for `block.basefee`. Quotes above the `uint96` escrow limit revert with `FeeOverflow` rather than truncating.
32
+
33
+ ## Paying for a request
34
+
35
+ `requestRandomness(clientSeed, callbackGasLimit, refundAddress)` and `requestMappedRandomness(..., spec)` accept `msg.value >= fee`, where `fee` is the quote computed inside that transaction. Less reverts with `IncorrectFee(expected, actual)`. Exactly `fee` is escrowed and stored as `requestFeePaid(requestId)`; `RandomnessRequested` emits that charged fee as `feePaid`, not `msg.value`. Anything above it is not revenue: it is credited to the request's `refundAddress` as refund credit (`FeeOverpaymentCredited(requestId, refundAddress, amount)`, readable through `refundCredits(address)`) and is withdrawn by that address calling `withdrawRefundCredit(recipient)`. Choose a refund address that can make that call, or that can receive a plain native transfer for expiry refunds; a contract that can do neither strands its credit.
36
+
37
+ ### Contracts that pay in the same transaction
38
+
39
+ `quoteFee(callbackGasLimit)` is exact inside the requesting transaction. `D20VRFRequests` helpers and `MiningRandomnessConsumer` pay it from the calling contract's balance:
40
+
41
+ ```solidity
42
+ uint256 fee = rng.quoteFee(callbackGasLimit);
43
+ requestId = rng.requestRandomness{value: fee}(clientSeed, callbackGasLimit, refundAddress);
44
+ ```
45
+
46
+ ### Wallets and backends that pay through a consumer
47
+
48
+ Do not call `quoteFee` through `eth_call`: it prices with `block.basefee`, which `eth_call` commonly reports as 0 (verified on Arc mainnet), so the answer collapses to `minFee` and the real transaction reverts with `IncorrectFee`. Quote with `quoteFeeAt(callbackGasLimit, latestBlock.baseFeePerGas)`, add a buffer for base-fee movement until inclusion, and forward the whole amount; the consumer example below does exactly that. The SDK helper wraps this for ethers 6:
49
+
50
+ ```js
51
+ import { quoteRequestFee } from '@d20dao/vrf-sdk';
52
+ // provider: ethers Provider; coordinator: coordinator proxy address; 100_000: callbackGasLimit
53
+ const { fee, value, baseFee } = await quoteRequestFee(provider, coordinator, 100_000, { bufferBps: 3000 });
54
+ await dice.roll(clientSeed, 100_000, { value });
55
+ ```
56
+
57
+ `fee` is `quoteFeeAt(callbackGasLimit, baseFee)` for the block's actual base fee. `value` is the same quote recomputed at a base fee `bufferBps` higher (default 3000, 30%: an EIP-1559 base fee can rise 12.5% per block), so the request still pays if the base fee rises by up to that much before inclusion. When the minimum fee dominates even at the buffered base fee, `value` equals `fee` and nothing extra is sent. In example A, `value` is 5 × 228.8 gwei × 400,000 = 0.4576 USDC; a request included at 176 gwei escrows 0.352 USDC and credits 0.1056 USDC to the refund address. The helper never uses `quoteFee`, needs only `getBlock` and `call`, and throws if the block has no `baseFeePerGas`. If the base fee outruns the buffer or pricing changes in between, the transaction reverts with `IncorrectFee`; quote again and resend.
58
+
59
+ ## Request lifecycle
60
+
61
+ Epochs last 200 blocks. The keeper selects one of four fixed recipes using the canonical block hash at epoch start minus one and prepares its first validated API3 snapshot locally. If the selected source yields no valid packet, the next source slot in a fixed order can be committed instead, one slot per 20-block window (at most three fallbacks); a saved response is never refreshed or resampled. Idle preparation publishes no transaction. An unused local snapshot can be retained for 50 epochs (10,000 blocks), subject to live-demand and unresolved-transaction protection.
62
+
63
+ A request escrows its quoted fee even when its epoch packet is not published yet, and fixes its original block, epoch, client seed, mapping, refund address, `feePaid`, `refundBps` and 60-second deadline. The keeper publishes the saved packet only for live paid demand. The randomness target becomes `max(requestBlock, committedBlock + 1)`, so its hash is unknown at publication; before publication the request has no usable target or VRF seed. Multiple requests share the packet, and timely requests can settle across epoch boundaries without changing their epoch.
64
+
65
+ Timely service is onchain proof acceptance at or before `requestedAt + 60` seconds; a pending transaction is not acceptance. At acceptance the keeper share, `keeperFeeBps` of `feePaid`, is paid to the registry's configured committer (never the proof submitter; a failed transfer becomes keeper credit) and the remainder becomes withdrawable protocol fees. With a 40% share, example A pays 0.1408 USDC to the keeper and 0.2112 USDC to the treasury. Callback failure still earns the fee; `retryCallback(requestId, gasLimit)` redelivers only the same accepted result and never pays a second share.
66
+
67
+ ### Expiry and refunds
68
+
69
+ After the deadline passes without an accepted proof, anyone may call `refundRequest(requestId)`. It pays `feePaid × refundBps / 10000` using the ratio snapshotted into the request at creation (`requestRefundBps(requestId)`), and the remainder becomes protocol fees. The ratio defaults to 100%; the owner can lower it with `setRefundBps` (event `RefundBpsChanged`) to no less than 50%, which affects only requests created afterwards. The refund is pushed to the fixed refund address with a 30,000-gas transfer; if that fails, the amount stays as refund credit for that address (`RequestRefundedTo(requestId, refundAddress, amount, paid)`) and is withdrawn with `withdrawRefundCredit`. Gas and application payments are not part of the refund. See [Optional refund notification](#optional-refund-notification) for the consumer hook.
70
+
71
+ ### Batched fulfillment
72
+
73
+ The keeper may fulfill up to 16 prepared requests in one transaction with `fulfillRandomnessBatch(ids, proofs)`. Every served member runs exactly like `fulfillRandomness`: its own `BlockHashStored`, `RequestServed`, `ProofVerified`, `RandomnessFulfilled`, `FulfillmentEvidence`, `CallbackAttempted` and `KeeperFeePaid` events, settlement from its own `feePaid` and its own callback. Members already fulfilled, refunded or past their deadline are left untouched and marked with `FulfillmentSkipped(requestId, reason)` (1 fulfilled, 2 refunded, 3 past deadline); a wrong seed, invalid proof or unready member reverts the whole batch. Consumers see no difference. Indexers and verifiers must read per-request events and the request's stored state, not transaction calldata: only a single `fulfillRandomness` call is 452 bytes.
74
+
75
+ ## Use locally
76
+
77
+ For SDK development, run `npm ci` and `npm test` from this repository. The test builds, packs and installs a real tarball in an isolated consumer, replays the recipe fixtures, type-checks a strict consumer, exercises `quoteRequestFee` against a mock provider and compiles the Solidity sources. `npm pack` also produces an installable local artifact.
78
+
79
+ ```js
80
+ import { builtins, mapRandomness, replayCoordinator, quoteRequestFee } from '@d20dao/vrf-sdk';
81
+ import { coordinatorAbi, epochEntropyAbi } from '@d20dao/vrf-sdk/abi';
82
+ const mapping = builtins.d20();
83
+ // Use only an independently verified accepted word for real outcomes.
84
+ ```
85
+
86
+ The root exports ESM and TypeScript declarations, including `quoteRequestFee`, `DEFAULT_FEE_BUFFER_BPS` and the `FeeQuote`, `FeeQuoteOptions` and `FeeQuoteProvider` types; `/epoch` exports epoch helpers and `MAX_ATTESTATION_AGE`. `/abi` exports `coordinatorAbi` and `epochEntropyAbi`, with JSON forms `D20VRFCoordinator.json` and `EpochEntropy.json`. The service implementations have locked empty constructors and explicit initializers. Registry initialization takes `address[4]`; it is not a four-address constructor deployment.
87
+
88
+ ## Integrate a consumer
89
+
90
+ Solidity imports require compiler 0.8.28 and your compiler's npm resolver:
91
+
92
+ - `@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol`
93
+ - `@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol`
94
+ - `@d20dao/vrf-sdk/contracts/libraries/D20VRFRequests.sol`
95
+ - `@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol`
96
+ - `@d20dao/vrf-sdk/contracts/examples/MiningRandomnessConsumer.sol`
97
+
98
+ `examples/DiceConsumer.sol` is one concrete consumer example for a player-paid request. It forwards the player's `msg.value` to `requestMappedRandomness`, so the coordinator escrows the exact same-transaction quote, credits any excess to the player as the fixed refund address and reverts underpayment with `IncorrectFee`. It stores the authenticated raw callback word; its mapped result is 1 through 20. Mapped callbacks still carry raw bytes32. Keep application actions separate from callbacks; the example does not implement application-payment refunds, claim locking or minting.
99
+
100
+ `D20VRFConsumer` authenticates the coordinator proxy. Verify the expected request in the callback and store the word with minimal work. Pin the effective coordinator proxy address, initialized configuration and implementation history of both service proxies. A constructor code-length check, SDK installation or permissionless request acceptance does not guarantee service.
101
+
102
+ ## Replay and verification
103
+
104
+ Use independently trusted successful receipts and state. Decode the registry `EpochCommitted` packet with `decodeEpochEvidencePacket` and verify with `replayEpochCommitment`, using the original source anchor, exact packet, commit block/time, ordered signers and registry identity. Decode the coordinator `FulfillmentEvidence` packet with `decodeEvidencePacket`, then call `replayCoordinator` with its exported input type (`Parameters<typeof replayCoordinator>[0]`).
105
+
106
+ `RequestContext` binds both `requestBlock` and `targetBlock`. Validate the epoch from the original request block, reconstruct the target from the actual publication block, and compare the event and stored transcript. Proof evidence is 416 bytes; fulfillment calldata is 452 bytes. Neither evidence packet has a version prefix. Choose the decoder from trusted emitter/event context. Decoding and mapping alone are not proof verification; replay does not authenticate RPC or establish receipt inclusion.
107
+
108
+ `EpochProtocolConfiguration` is the initialized configuration: `feeRecipient` from `initialFeeRecipient()`, `initialMinFee` from `initialMinFee()` (the `initialize` fee argument), `catalogHash` from `catalogHash()`. Live `pricing()`, `feeRecipient()` and scheduled catalogs never change `protocolConfigurationHash`.
109
+
110
+ Signer catalogs are per epoch. The registry owner can schedule a replacement catalog with `scheduleCatalog(signers, fromEpoch)` for epochs at least two ahead (event `CatalogScheduled(fromEpoch, catalogHash, signers)`); the current and next epoch, prepared snapshots and open requests keep their signers. `catalogHashAt(epochId)` and `signersAt(epochId)` return the catalog in force for an epoch, and `Epoch.catalogHash` records it at commitment. For replay, `epoch.catalog.signers` must be that per-epoch catalog, taken from `signersAt` or the `CatalogScheduled` history, while `configuration.catalogHash` stays the initial catalog bound into the configuration hash; `replayEpochCommitment` binds the supplied signers to `record.catalogHash`. `catalogHash()` and the slot getters always return the initial catalog.
111
+
112
+ At publication a signed attestation may be at most 240 seconds old and never future-dated (`MAX_ATTESTATION_AGE`, exported from `/epoch`); `replayEpochCommitment` enforces the same bound against the commit timestamp.
113
+
114
+ D20VRFCoordinator and EpochEntropy use atomically initialized ERC1967 proxies with owner-authorized UUPS upgrades and two-step ownership transfers; `renounceOwnership` reverts on both, so upgrade authority can only move through an accepted transfer. The registry owner can change the committer and schedule future catalogs; the coordinator owner can change fee recipient, keeper share, bounded pricing and the refund ratio. No setter rewrites a request, a published epoch or the VRF key, but upgrade authority can change code and is an explicit trust assumption. Verify the implementation history of BOTH proxies at the relevant receipts; stable proxy addresses alone do not identify executed code. Operators pin the proxy code, initialized configuration and both implementation addresses/runtime hashes; the keeper fails closed on an unreviewed implementation change.
115
+
116
+ ## Operational and release boundary
117
+
118
+ This SDK contains no keeper service, API fetching, proof generation, signer secrets or deployment automation. The canonical keeper has a Docker install wrapper that builds, provisions separately supplied key files and starts from reviewed configuration; inspect its platform-specific guide before use. No deployment or funding is authorized by SDK installation.
119
+
120
+ Optional Telegram access is disabled unless a bot token and numeric operator chat are explicitly configured. Only that chat can use read-only /status and /keeper commands. Commands never modify configuration or send transactions; notifications are best-effort observations, not chain evidence. This package neither reads bot credentials nor contacts Telegram.
121
+
122
+ Builds use reviewed protocol Git blobs and verify every SHA-256 in PROTOCOL-PROVENANCE.json. `src/fees.ts` (the fee-quoting helper) is SDK-owned rather than vendored; BUILD-MANIFEST.json records it under `packageSources` next to the protocol source, dependency-lock and imported OpenZeppelin hashes. The UUPS build uses OpenZeppelin contracts and contracts-upgradeable 5.6.1. Consumer source is copied exactly; service implementations, operator code, test fixtures and provers are excluded from the tarball.
123
+
124
+ Fixture provenance distinguishes explicit CI signatures from actual API3 responses. Fixtures are not included in the package. The browser-target bundle is executed under Node, not an actual browser session; independently trusted chain context is still required for real verification.
125
+
126
+ SDK installation provides consumer and verification tooling. Chain availability, provider quotas, upgrade administration and application settlement remain separate concerns. A healthy process alone does not guarantee a particular request's timely fulfillment.
127
+
128
+ ## Deployments
129
+
130
+ Obtain proxy addresses, implementation addresses and independently checked code hashes from the keeper's deployment manifests, and check that the coordinator implementation at your chain's proxy exposes `quoteFee`/`quoteFeeAt` (its code hash matches the manifest entry for this protocol version) before relying on this SDK's interface. The testnet upgrade to this protocol version and the Arc mainnet deployment are published there when confirmed.
131
+
132
+ ### Arc Testnet
133
+
134
+ Chain ID: **5042002**. Use the **coordinator proxy** when constructing a consumer.
135
+
136
+ | Contract | Role | Arc Testnet address |
137
+ | --- | --- | --- |
138
+ | D20VRFCoordinator | Consumer entry point / proxy | [`0xd20DA0FF9087d053f0291524Eac12abA1ADBd945`](https://testnet.arcscan.app/address/0xd20DA0FF9087d053f0291524Eac12abA1ADBd945) |
139
+ | EpochEntropy | Epoch registry / proxy | [`0xD20Da00B47A7cD2211dC4683E306913b05903756`](https://testnet.arcscan.app/address/0xD20Da00B47A7cD2211dC4683E306913b05903756) |
140
+ | D20CostClient | Restricted cost client / proxy | [`0xD20da026090B8472579a2B93030F1fC4c94807F1`](https://testnet.arcscan.app/address/0xD20da026090B8472579a2B93030F1fC4c94807F1) |
141
+ | D20VRFCoordinator | Implementation | [`0xD20da0c375cEfCdA65703699A4090237057e9b68`](https://testnet.arcscan.app/address/0xD20da0c375cEfCdA65703699A4090237057e9b68) |
142
+ | EpochEntropy | Implementation | [`0xD20Da0cf7Ddc6123f9A87c0C210F8ECB934CA7D5`](https://testnet.arcscan.app/address/0xD20Da0cf7Ddc6123f9A87c0C210F8ECB934CA7D5) |
143
+ | D20CostClient | Implementation | [`0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b`](https://testnet.arcscan.app/address/0xD20DA00A872acfDe3e4721Fc1051BD23CC84B66b) |
144
+
145
+ Addresses are copied from the [Arc Testnet deployment manifest](https://github.com/d20dao/keeper/blob/main/deployments/arc-testnet.json). Explorer links identify addresses; they do not assert explorer source-code verification. Implementation addresses change through owner-authorized upgrades, so the implementation rows and code hashes are only valid together with the manifest revision they came from. The pilot consumer is test tooling, not a shared application entry point. The [testnet stress run](https://github.com/d20dao/keeper/blob/main/docs/benchmarks/arc-testnet-stress-2026-09-16.json) served 68 paid requests within 2–4 chain seconds, 47 of them in batched fulfillments; measured timings are not an SLA.
146
+
147
+ ## Optional refund notification
148
+
149
+ After `refundRequest` has paid the fixed refund address or recorded its refund credit, the coordinator calls `onRefund(requestId)` on the original consumer. Extend `D20VRFConsumer` and override `_onRefund(uint256 requestId)` to update application state; the base authenticates the coordinator. The callback only carries the request ID and does not imply that the consumer itself received money. Application assets and fees remain the application's responsibility.
150
+
151
+ The first attempt forwards 100,000 gas. A reverting or gas-exhausting hook cannot undo the fee settlement. After failure, `retryRefundCallback(requestId, gasLimit)` retries the notification without another payment; successful delivery is recorded by `refundCallbackDelivered(requestId)`. Refund/retry needs sufficient outer gas. Never request new randomness from within either callback; use a separate application transaction.
@@ -1,6 +1,6 @@
1
1
  # Third-party attribution
2
2
 
3
- The redistributed consumer Solidity files and public TypeScript helpers are from this repository under its MIT LICENSE. The coordinator ABI is generated, not hand-maintained. The coordinator implementation and Chainlink Solidity verifier are not distributed as SDK runtime or import sources.
3
+ The redistributed consumer Solidity files and public TypeScript helpers are from this repository under its MIT LICENSE: the protocol sources are vendored from d20dao/keeper as recorded in PROTOCOL-PROVENANCE.json, and the fee-quoting helper (`src/fees.ts`) is maintained here. The coordinator and registry ABIs are generated, not hand-maintained. The coordinator implementation and Chainlink Solidity verifier are not distributed as SDK runtime or import sources.
4
4
 
5
5
  Public proof verification implements compatibility with the pinned Chainlink secp256k1/Keccak construction. Its upstream provenance and full preserved root license are included in `notices/PROVENANCE.md` and `notices/CHAINLINK-LICENSE`. The provenance path describes the original repository, not a bundled verifier. This is not a Chainlink service or an extension of an upstream audit.
6
6