@d20dao/vrf-sdk 0.3.4 → 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.
@@ -0,0 +1,220 @@
1
+ import { getBytes, hexlify, toUtf8Bytes, toUtf8String } from "ethers";
2
+ /// Data templates: the exact signed-data grammar of an epoch recipe, with the verdicts of EpochEntropy's DataTemplate.
3
+ /// A template is a byte sequence of segments, each an opcode and its operands:
4
+ /// 0x01 LITERAL len bytes[len] exactly these bytes (1 <= len <= 128)
5
+ /// 0x02 HEX n exactly n characters 0-9 or a-f (1 <= n <= 128)
6
+ /// 0x03 DECIMAL flags unsigned JSON number: 0 or a nonzero digit then digits, then an optional fraction
7
+ /// when flags & 1 and an optional exponent when flags & 2 (flags <= 3)
8
+ /// 0x04 INTEGER min max a nonzero digit then digits, min <= digit count <= max (1 <= min <= max <= 128)
9
+ /// Variable segments are greedy and never backtrack; data matches when the segments consume it exactly. A well-formed
10
+ /// template has at most 256 bytes, at least one variable segment, and a shortest match of at most 128 bytes.
11
+ export const MAX_EPOCH_DATA_BYTES = 128, MAX_DATA_TEMPLATE_BYTES = 256;
12
+ const LITERAL = 0x01, HEX = 0x02, DECIMAL = 0x03, INTEGER = 0x04, FRACTION = 0x01, EXPONENT = 0x02;
13
+ const byteOperand = (value, name) => {
14
+ if (!Number.isInteger(value) || value < 1 || value > MAX_EPOCH_DATA_BYTES)
15
+ throw new Error(`Data template ${name} must be an integer from 1 to ${MAX_EPOCH_DATA_BYTES}`);
16
+ return value;
17
+ };
18
+ /// Encode readable segments as template bytes; throws with the rule a segment breaks.
19
+ export function encodeDataTemplate(segments) {
20
+ if (!Array.isArray(segments))
21
+ throw new Error("A data template is a list of segments");
22
+ const bytes = [];
23
+ segments.forEach((segment, index) => {
24
+ const keys = segment !== null && typeof segment === "object" ? Object.keys(segment) : [];
25
+ if (keys.length !== 1)
26
+ throw new Error(`Data template segment ${index} must have exactly one of literal, hex, decimal or integer`);
27
+ if ("literal" in segment) {
28
+ if (typeof segment.literal !== "string")
29
+ throw new Error(`Data template segment ${index}: literal must be a string`);
30
+ const text = toUtf8Bytes(segment.literal);
31
+ byteOperand(text.length, `segment ${index} literal length in bytes`);
32
+ bytes.push(LITERAL, text.length, ...text);
33
+ }
34
+ else if ("hex" in segment) {
35
+ bytes.push(HEX, byteOperand(segment.hex, `segment ${index} hex length`));
36
+ }
37
+ else if ("decimal" in segment) {
38
+ const { fraction, exponent } = segment.decimal ?? {};
39
+ if (typeof fraction !== "boolean" || typeof exponent !== "boolean" || Object.keys(segment.decimal).length !== 2)
40
+ throw new Error(`Data template segment ${index}: decimal needs boolean fraction and exponent`);
41
+ bytes.push(DECIMAL, (fraction ? FRACTION : 0) | (exponent ? EXPONENT : 0));
42
+ }
43
+ else if ("integer" in segment) {
44
+ const { minDigits, maxDigits } = segment.integer ?? {};
45
+ byteOperand(minDigits, `segment ${index} integer minDigits`);
46
+ byteOperand(maxDigits, `segment ${index} integer maxDigits`);
47
+ if (minDigits > maxDigits || Object.keys(segment.integer).length !== 2)
48
+ throw new Error(`Data template segment ${index}: integer needs minDigits <= maxDigits`);
49
+ bytes.push(INTEGER, minDigits, maxDigits);
50
+ }
51
+ else
52
+ throw new Error(`Data template segment ${index} must have exactly one of literal, hex, decimal or integer`);
53
+ });
54
+ const template = hexlify(Uint8Array.from(bytes));
55
+ const problem = templateProblem(Uint8Array.from(bytes));
56
+ if (problem)
57
+ throw new Error(`Invalid data template: ${problem}`);
58
+ return template;
59
+ }
60
+ /// Why a template is not well-formed, or undefined when it is. Same rules as DataTemplate.isValid.
61
+ function templateProblem(t) {
62
+ if (t.length === 0 || t.length > MAX_DATA_TEMPLATE_BYTES)
63
+ return `it must be 1 to ${MAX_DATA_TEMPLATE_BYTES} bytes`;
64
+ let at = 0, shortest = 0, variable = false;
65
+ while (at < t.length) {
66
+ const op = t[at];
67
+ if (op === LITERAL) {
68
+ if (at + 1 >= t.length)
69
+ return `truncated literal at byte ${at}`;
70
+ const n = t[at + 1];
71
+ if (n === 0 || n > MAX_EPOCH_DATA_BYTES || at + 2 + n > t.length)
72
+ return `invalid literal length at byte ${at}`;
73
+ shortest += n;
74
+ at += 2 + n;
75
+ }
76
+ else if (op === HEX) {
77
+ if (at + 1 >= t.length)
78
+ return `truncated hex segment at byte ${at}`;
79
+ const n = t[at + 1];
80
+ if (n === 0 || n > MAX_EPOCH_DATA_BYTES)
81
+ return `invalid hex length at byte ${at}`;
82
+ shortest += n;
83
+ at += 2;
84
+ variable = true;
85
+ }
86
+ else if (op === DECIMAL) {
87
+ if (at + 1 >= t.length || t[at + 1] > (FRACTION | EXPONENT))
88
+ return `invalid decimal flags at byte ${at}`;
89
+ shortest += 1;
90
+ at += 2;
91
+ variable = true;
92
+ }
93
+ else if (op === INTEGER) {
94
+ if (at + 2 >= t.length)
95
+ return `truncated integer segment at byte ${at}`;
96
+ const min = t[at + 1], max = t[at + 2];
97
+ if (min === 0 || min > max || max > MAX_EPOCH_DATA_BYTES)
98
+ return `invalid integer digit bounds at byte ${at}`;
99
+ shortest += min;
100
+ at += 3;
101
+ variable = true;
102
+ }
103
+ else
104
+ return `unknown segment opcode ${op} at byte ${at}`;
105
+ }
106
+ if (!variable)
107
+ return "it needs at least one hex, decimal or integer segment";
108
+ if (shortest > MAX_EPOCH_DATA_BYTES)
109
+ return `its shortest match exceeds ${MAX_EPOCH_DATA_BYTES} bytes`;
110
+ return undefined;
111
+ }
112
+ export function isValidDataTemplate(template) { return templateProblem(getBytes(template)) === undefined; }
113
+ /// Throws unless the template is well-formed.
114
+ export function validateDataTemplate(template) {
115
+ const problem = templateProblem(getBytes(template));
116
+ if (problem)
117
+ throw new Error(`Invalid data template: ${problem}`);
118
+ }
119
+ /// The readable segments of a well-formed template whose literals are UTF-8 text.
120
+ export function decodeDataTemplate(template) {
121
+ const t = getBytes(template);
122
+ validateDataTemplate(t);
123
+ const segments = [];
124
+ for (let at = 0; at < t.length;) {
125
+ const op = t[at];
126
+ if (op === LITERAL) {
127
+ segments.push({ literal: toUtf8String(t.slice(at + 2, at + 2 + t[at + 1])) });
128
+ at += 2 + t[at + 1];
129
+ }
130
+ else if (op === HEX) {
131
+ segments.push({ hex: t[at + 1] });
132
+ at += 2;
133
+ }
134
+ else if (op === DECIMAL) {
135
+ segments.push({ decimal: { fraction: (t[at + 1] & FRACTION) !== 0, exponent: (t[at + 1] & EXPONENT) !== 0 } });
136
+ at += 2;
137
+ }
138
+ else {
139
+ segments.push({ integer: { minDigits: t[at + 1], maxDigits: t[at + 2] } });
140
+ at += 3;
141
+ }
142
+ }
143
+ return segments;
144
+ }
145
+ const isDigit = (c) => c !== undefined && c >= 0x30 && c <= 0x39;
146
+ function digits(d, p) { while (isDigit(d[p]))
147
+ p++; return p; }
148
+ /// End of an unsigned JSON number starting at p, or -1.
149
+ function decimalEnd(d, p, fraction, exponent) {
150
+ if (!isDigit(d[p]))
151
+ return -1;
152
+ if (d[p] === 0x30) {
153
+ p++;
154
+ if (isDigit(d[p]))
155
+ return -1;
156
+ }
157
+ else
158
+ p = digits(d, p);
159
+ if (fraction && d[p] === 0x2e) {
160
+ const first = p + 1;
161
+ p = digits(d, first);
162
+ if (p === first)
163
+ return -1;
164
+ }
165
+ if (exponent && (d[p] === 0x65 || d[p] === 0x45)) {
166
+ p++;
167
+ if (d[p] === 0x2b || d[p] === 0x2d)
168
+ p++;
169
+ const first = p;
170
+ p = digits(d, first);
171
+ if (p === first)
172
+ return -1;
173
+ }
174
+ return p;
175
+ }
176
+ /// Whether data is exactly a record the template describes; false for a malformed template.
177
+ export function matchesDataTemplate(template, data) {
178
+ const t = getBytes(template), d = getBytes(data);
179
+ if (templateProblem(t) !== undefined || d.length === 0 || d.length > MAX_EPOCH_DATA_BYTES)
180
+ return false;
181
+ let p = 0;
182
+ for (let at = 0; at < t.length;) {
183
+ const op = t[at];
184
+ if (op === LITERAL) {
185
+ const n = t[at + 1];
186
+ if (p + n > d.length)
187
+ return false;
188
+ for (let i = 0; i < n; i++)
189
+ if (d[p + i] !== t[at + 2 + i])
190
+ return false;
191
+ p += n;
192
+ at += 2 + n;
193
+ }
194
+ else if (op === HEX) {
195
+ const end = p + t[at + 1];
196
+ if (end > d.length)
197
+ return false;
198
+ for (; p < end; p++)
199
+ if (!isDigit(d[p]) && (d[p] < 0x61 || d[p] > 0x66))
200
+ return false;
201
+ at += 2;
202
+ }
203
+ else if (op === DECIMAL) {
204
+ p = decimalEnd(d, p, (t[at + 1] & FRACTION) !== 0, (t[at + 1] & EXPONENT) !== 0);
205
+ if (p < 0)
206
+ return false;
207
+ at += 2;
208
+ }
209
+ else {
210
+ if (!isDigit(d[p]) || d[p] === 0x30)
211
+ return false;
212
+ const end = digits(d, p), count = end - p;
213
+ if (count < t[at + 1] || count > t[at + 2])
214
+ return false;
215
+ p = end;
216
+ at += 3;
217
+ }
218
+ }
219
+ return p === d.length;
220
+ }
@@ -3,48 +3,56 @@ pragma solidity 0.8.28;
3
3
 
4
4
  import {D20VRFConsumer} from "@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol";
5
5
  import {ID20VRF} from "@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol";
6
- import {RandomnessMapping} from "@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol";
7
-
8
- /// @notice Minimal request/storage starter; no game payment, proof, mint or deployment policy.
9
- /// @dev The player pays the request. A wallet cannot know the exact same-transaction quote in advance, so it quotes
10
- /// off-chain with quoteFeeAt(callbackGasLimit, latestBlock.baseFeePerGas) plus a buffer (the SDK's quoteRequestFee
11
- /// helper does this) and sends that value. The whole payment is forwarded: the coordinator escrows exactly
12
- /// quoteFee(callbackGasLimit) and credits any excess to the player (the refund address) as refund credit that only
13
- /// the player can pull with withdrawRefundCredit. Underpayment reverts inside the coordinator with
14
- /// IncorrectFee(expected, actual). A contract that funds requests from its own balance would instead pay
15
- /// rng.quoteFee(callbackGasLimit) in the same transaction, as the D20VRFRequests helpers do.
6
+ import {D20VRFRequests} from "@d20dao/vrf-sdk/contracts/libraries/D20VRFRequests.sol";
7
+
8
+ /// @notice One d20 per player, paid by the player. Swap `d20` for `d6` or `coinFlip` and nothing else moves.
16
9
  contract DiceConsumer is D20VRFConsumer {
17
- struct Roll { address player; bytes32 rawWord; bool ready; }
10
+ using D20VRFRequests for ID20VRF;
11
+
12
+ /// The callback does two storage writes and nothing else, and the fee grows with this number.
13
+ uint32 private constant CALLBACK_GAS = 100_000;
14
+
15
+ struct Roll { address player; bytes32 word; bool ready; }
18
16
  mapping(uint256 => Roll) public rolls;
19
- mapping(uint256 => bool) public refunded;
17
+
18
+ error Underpaid(uint256 quoted, uint256 sent);
19
+ error ChangeRefused();
20
20
  error UnexpectedCallback();
21
21
  error NotReady();
22
22
 
23
23
  constructor(address coordinator) D20VRFConsumer(coordinator) {}
24
24
 
25
- function roll(bytes32 clientSeed, uint32 callbackGasLimit) external payable returns (uint256 requestId) {
26
- // One d20. The callback still receives the raw word; result() reads the canonical mapped value.
27
- RandomnessMapping.Spec memory spec = RandomnessMapping.Spec(RandomnessMapping.Operation.DiceRoll, 0, 20, 1, 0);
28
- requestId = ID20VRF(vrfCoordinator).requestMappedRandomness{value: msg.value}(
29
- clientSeed, callbackGasLimit, msg.sender, spec
30
- );
25
+ function roll() external payable returns (uint256 requestId) {
26
+ ID20VRF rng = ID20VRF(vrfCoordinator);
27
+ // quoteFee prices from block.basefee, so it is exact here and only here. A wallet cannot read it in
28
+ // advance: off-chain it quotes quoteFeeAt(gas, the latest header's baseFeePerGas) plus a buffer and
29
+ // sends that, never through eth_call, where the base fee is reported as 0.
30
+ uint256 fee = rng.quoteFee(CALLBACK_GAS);
31
+ if (msg.value < fee) revert Underpaid(fee, msg.value);
32
+ // The helper pays exactly `fee` out of this contract's balance, which msg.value just funded.
33
+ // The player is the refund address, so if the request expires, refundRequest returns the fee straight to them.
34
+ requestId = rng.d20(D20VRFRequests.Options(keccak256(abi.encode(msg.sender)), CALLBACK_GAS, msg.sender));
31
35
  rolls[requestId] = Roll(msg.sender, bytes32(0), false);
36
+ // Hand the player's buffer back now. Left with the coordinator it becomes refund credit they would
37
+ // have to claim in a separate withdrawRefundCredit transaction.
38
+ if (msg.value > fee) {
39
+ (bool sent,) = payable(msg.sender).call{value: msg.value - fee}("");
40
+ if (!sent) revert ChangeRefused();
41
+ }
32
42
  }
33
43
 
34
44
  function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
35
- Roll storage result = rolls[requestId];
36
- if (result.player == address(0) || result.ready || refunded[requestId]) revert UnexpectedCallback();
37
- result.rawWord = randomness;
38
- result.ready = true;
39
- }
40
-
41
- // Called after an expired request's refund share was paid to the player or recorded as the player's refund credit.
42
- function _onRefund(uint256 requestId) internal override {
43
- Roll storage result = rolls[requestId];
44
- if (result.player == address(0) || result.ready) revert UnexpectedCallback();
45
- refunded[requestId] = true;
45
+ Roll storage entry = rolls[requestId];
46
+ // Refuse a request this contract never made, and a repeat of one it already holds. A failed delivery is
47
+ // retried by anyone with retryCallback and a successful one is never repeated; the check costs one read.
48
+ if (entry.player == address(0) || entry.ready) revert UnexpectedCallback();
49
+ // Store, do not compute. The callback runs inside CALLBACK_GAS; work that overruns it fails the
50
+ // delivery, and although the request stays served and paid, someone must retryCallback it.
51
+ entry.word = randomness;
52
+ entry.ready = true;
46
53
  }
47
54
 
55
+ /// @return 1 to 20. The coordinator maps the stored word; a losing roll is never re-rolled.
48
56
  function result(uint256 requestId) external view returns (uint256) {
49
57
  if (!rolls[requestId].ready) revert NotReady();
50
58
  return ID20VRF(vrfCoordinator).getMappedResult(requestId)[0];
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {D20VRFConsumer} from "@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol";
5
+ import {ID20VRF} from "@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol";
6
+ import {RandomnessMapping} from "@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol";
7
+
8
+ /// @notice A weighted drop: common 60%, uncommon 25%, rare 13%, legendary 2%. Edit weights and TOTAL_WEIGHT
9
+ /// together to move the odds; nothing else changes.
10
+ contract LootDropConsumer is D20VRFConsumer {
11
+ uint32 private constant CALLBACK_GAS = 100_000;
12
+ uint256 private constant TOTAL_WEIGHT = 1000;
13
+ /// Tier 0 to 3, rarest last. Must sum to TOTAL_WEIGHT.
14
+ uint256[4] private weights = [uint256(600), 250, 130, 20];
15
+
16
+ struct Drop { address player; bytes32 word; bool ready; }
17
+ mapping(uint256 => Drop) public drops;
18
+
19
+ error UnexpectedCallback();
20
+ error NotReady();
21
+
22
+ constructor(address coordinator) D20VRFConsumer(coordinator) {}
23
+
24
+ function open() external payable returns (uint256 requestId) {
25
+ // One draw in [1, TOTAL_WEIGHT]. The coordinator samples the range without modulo bias and stores the
26
+ // mapping with the request, so the draw is read back from getMappedResult and anyone can replay it.
27
+ // Asking for the range you actually want is what makes the weights below mean what they say.
28
+ RandomnessMapping.Spec memory spec =
29
+ RandomnessMapping.Spec(RandomnessMapping.Operation.NumberRange, 1, TOTAL_WEIGHT, 1, 0);
30
+ // Forwards everything sent: the coordinator keeps exactly its quote for this transaction, reverts
31
+ // IncorrectFee if that is more than arrived, and credits the surplus to msg.sender as refund credit.
32
+ requestId = ID20VRF(vrfCoordinator).requestMappedRandomness{value: msg.value}(
33
+ keccak256(abi.encode(msg.sender, block.number)), CALLBACK_GAS, msg.sender, spec
34
+ );
35
+ drops[requestId] = Drop(msg.sender, bytes32(0), false);
36
+ }
37
+
38
+ function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
39
+ Drop storage drop = drops[requestId];
40
+ // Unknown request, or a repeat of one already delivered: neither may touch a finished drop.
41
+ if (drop.player == address(0) || drop.ready) revert UnexpectedCallback();
42
+ // Store the word and stop. The tier walk below is cheap, but it belongs on the reading side: a
43
+ // callback that runs out of CALLBACK_GAS fails delivery, while a stored word can be read forever.
44
+ drop.word = randomness;
45
+ drop.ready = true;
46
+ }
47
+
48
+ /// @return tier 0 common, 1 uncommon, 2 rare, 3 legendary. Read-only: one request, one answer, forever.
49
+ function tierOf(uint256 requestId) external view returns (uint256 tier) {
50
+ if (!drops[requestId].ready) revert NotReady();
51
+ uint256 draw = ID20VRF(vrfCoordinator).getMappedResult(requestId)[0];
52
+ uint256 cursor;
53
+ // Weights become adjacent ranges: 1-600 common, 601-850 uncommon, 851-980 rare, the rest legendary.
54
+ for (uint256 i; i + 1 < weights.length; ++i) {
55
+ cursor += weights[i];
56
+ if (draw <= cursor) return i;
57
+ }
58
+ return weights.length - 1;
59
+ }
60
+ }
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {D20VRFConsumer} from "@d20dao/vrf-sdk/contracts/D20VRFConsumer.sol";
5
+ import {ID20VRF} from "@d20dao/vrf-sdk/contracts/interfaces/ID20VRF.sol";
6
+ import {RandomnessMapping} from "@d20dao/vrf-sdk/contracts/libraries/RandomnessMapping.sol";
7
+
8
+ /// @notice One winner drawn from a list of entrants. Swap ChooseOne for ChooseMany to draw several.
9
+ contract RaffleConsumer is D20VRFConsumer {
10
+ uint32 private constant CALLBACK_GAS = 100_000;
11
+
12
+ address[] public entrants;
13
+ /// The outstanding or served request: zero before the first draw, and zero again once an expired one was refunded.
14
+ uint256 public drawId;
15
+ bytes32 public word;
16
+ bool public closed; bool public drawn;
17
+
18
+ error AlreadyClosed();
19
+ error NoEntrants();
20
+ error RaffleFull();
21
+ error AlreadyDrawn();
22
+ error UnexpectedCallback();
23
+ error NotDrawn();
24
+
25
+ constructor(address coordinator) D20VRFConsumer(coordinator) {}
26
+
27
+ function enter() external {
28
+ if (closed) revert AlreadyClosed();
29
+ // ChooseOne answers with an index, and the coordinator maps at most 256 items.
30
+ if (entrants.length == 256) revert RaffleFull();
31
+ entrants.push(msg.sender);
32
+ }
33
+
34
+ function draw() external payable {
35
+ // A raffle that can be drawn twice is not a raffle: one served request, one winner, and no second
36
+ // request while one is outstanding or after one was served.
37
+ if (drawId != 0) revert AlreadyDrawn();
38
+ if (entrants.length == 0) revert NoEntrants();
39
+ closed = true;
40
+ // The answer is an index, so the list must be frozen before the request goes out; hashing it into the
41
+ // client seed publishes that promise, because RandomnessRequested logs the seed. msg.value is
42
+ // forwarded whole: the coordinator keeps exactly its quote, reverts IncorrectFee when that is more
43
+ // than arrived, and credits the surplus to msg.sender as refund credit to withdraw later.
44
+ drawId = ID20VRF(vrfCoordinator).requestMappedRandomness{value: msg.value}(
45
+ keccak256(abi.encode(entrants)), CALLBACK_GAS, msg.sender,
46
+ RandomnessMapping.Spec(RandomnessMapping.Operation.ChooseOne, 0, 0, 1, uint32(entrants.length))
47
+ );
48
+ }
49
+
50
+ function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
51
+ // Only the one request this raffle made, and only once: a failed delivery is retried by anyone with
52
+ // retryCallback, and ids this contract never used are not its business.
53
+ if (requestId != drawId || drawn) revert UnexpectedCallback();
54
+ // Store and stop, so the callback cannot run out of its budget. Paying the prize belongs in a
55
+ // separate transaction anyone can send once `drawn` is true.
56
+ word = randomness;
57
+ drawn = true;
58
+ }
59
+
60
+ /// If no proof is accepted within 60 seconds the request expires and anyone may call refundRequest(drawId)
61
+ /// on the coordinator. That returns the fee to whoever sent draw() and then calls this hook: the list stays
62
+ /// frozen and draw() may be sent again, so an expired request never leaves the raffle stuck.
63
+ function _onRefund(uint256 requestId) internal override {
64
+ if (requestId == drawId && !drawn) drawId = 0;
65
+ }
66
+
67
+ function winner() external view returns (address) {
68
+ if (!drawn) revert NotDrawn();
69
+ return entrants[ID20VRF(vrfCoordinator).getMappedResult(drawId)[0]];
70
+ }
71
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Kevin Charm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -9,3 +9,15 @@
9
9
  - Modifications: none. `npm run vendor:check` verifies the original bytes.
10
10
 
11
11
  This is the Chainlink secp256k1/Keccak VRF construction described in that source, with its documented differences from the referenced IETF draft. It is not advertised as RFC 9381 wire-format interoperability. Reusing this file neither makes D20DAO a Chainlink service nor extends any upstream audit to our coordinator, mapping, consumer or keeper.
12
+
13
+ # Vendored BLS library on BN254
14
+
15
+ - Upstream: https://github.com/kevincharm/bls-bn254
16
+ - Commit: `9f70fb4dff2cd0921dc2144929dc0aff6f21a9b9` (release 2.0.0)
17
+ - Files, in `bls-bn254/`:
18
+ - `contracts/BLS.sol`, SHA-256 `887fd94553b9d6d0a247900bdb05523d3b9ef3b8ca049e2a0524a0b1a9d37e69`
19
+ - `contracts/ModExp.sol`, SHA-256 `fd91ae9291511668914b05fece03fb9bf6e2b57f7784d2edddc82456af12b495`
20
+ - License: MIT, preserved in `bls-bn254/LICENSE`, SHA-256 `af36460fa628a7aca8c5b1f1b6b3615376f00212284fa0fd61a013ee982a3666`.
21
+ - Modifications: none. `npm run vendor:check` verifies the original bytes, the license included.
22
+
23
+ `D20BeaconVerifier` uses this library's RFC 9380 hash-to-curve onto G1 (expand_message_xmd with keccak256 and the Shallue-van de Woestijne map) and its point checks. These are the operations of drand's `bls-bn254-unchained-on-g1` scheme. The verifier runs the pairing itself, with a fixed gas allowance, instead of the library's `verifySingle`, which forwards all remaining gas to the precompile. Reusing this file neither makes D20DAO a drand or League of Entropy service nor extends any upstream review to our registry or keeper.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@d20dao/vrf-sdk",
3
- "version": "0.3.4",
4
- "description": "D20DAO verifiable randomness: Solidity consumer helpers, contract ABIs and public proof replay",
3
+ "version": "0.5.0",
4
+ "description": "Verifiable randomness (VRF) for onchain apps: Solidity consumers, contract ABIs, USDC fee quotes and public proof replay. Live on Arc.",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "registry": "https://registry.npmjs.org/"
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "./abi/D20VRFCoordinator.json": "./abi/D20VRFCoordinator.json",
30
30
  "./abi/EpochEntropy.json": "./abi/EpochEntropy.json",
31
+ "./abi/D20BeaconVerifier.json": "./abi/D20BeaconVerifier.json",
31
32
  "./contracts/*": "./contracts/*",
32
33
  "./examples/*": "./examples/*",
33
34
  "./AGENTS.md": "./AGENTS.md",
@@ -40,13 +41,15 @@
40
41
  "examples/*.sol",
41
42
  "AGENTS.md",
42
43
  "API.md",
44
+ "CHANGELOG.md",
43
45
  "LICENSE",
44
46
  "THIRD_PARTY_NOTICES.md",
45
47
  "notices/*",
46
48
  "BUILD-MANIFEST.json",
47
49
  "PROTOCOL-PROVENANCE.json",
48
50
  "abi/D20VRFCoordinator.json",
49
- "abi/EpochEntropy.json"
51
+ "abi/EpochEntropy.json",
52
+ "abi/D20BeaconVerifier.json"
50
53
  ],
51
54
  "scripts": {
52
55
  "build": "node scripts/build.mjs",
@@ -75,5 +78,29 @@
75
78
  "repository": {
76
79
  "type": "git",
77
80
  "url": "https://github.com/d20dao/d20-sdk.git"
78
- }
81
+ },
82
+ "homepage": "https://d20dao.org",
83
+ "bugs": {
84
+ "url": "https://github.com/d20dao/d20-sdk/issues"
85
+ },
86
+ "keywords": [
87
+ "vrf",
88
+ "verifiable-random-function",
89
+ "verifiable-randomness",
90
+ "randomness",
91
+ "random-number",
92
+ "rng",
93
+ "oracle",
94
+ "arc",
95
+ "arc-network",
96
+ "usdc",
97
+ "solidity",
98
+ "smart-contracts",
99
+ "evm",
100
+ "ethers",
101
+ "onchain",
102
+ "dice",
103
+ "raffle",
104
+ "d20dao"
105
+ ]
79
106
  }
@@ -1,59 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {D20VRFConsumer} from "../D20VRFConsumer.sol";
5
- import {ID20VRF} from "../interfaces/ID20VRF.sol";
6
-
7
- /// @notice Integration building block, not a replacement for the game's mining/economy contracts.
8
- /// @dev The actual game validates PoW, locks payment and immutable claim attributes BEFORE
9
- /// calling _requestForClaim inside that same transaction. No user reveal/contribution call.
10
- abstract contract MiningRandomnessConsumer is D20VRFConsumer {
11
- struct ClaimRandomness { uint256 requestId; bytes32 randomness; bool requested; bool ready; }
12
- mapping(uint256 => ClaimRandomness) public claimRandomness;
13
- mapping(uint256 => uint256) public requestToClaim;
14
- mapping(uint256 => bool) private knownRequest;
15
- event ClaimRandomnessRequested(uint256 indexed claimId, uint256 indexed requestId);
16
- event ClaimReady(uint256 indexed claimId, uint256 indexed requestId, bytes32 randomness);
17
- error ClaimAlreadyRequested();
18
- error UnexpectedCallback();
19
- error ClaimNotReady();
20
- error InvalidCandidate();
21
-
22
- constructor(address coordinator) D20VRFConsumer(coordinator) {}
23
-
24
- function _requestForClaim(uint256 claimId, bytes32 lockedWork, uint32 callbackGasLimit, address refundAddress)
25
- internal returns (uint256 requestId)
26
- {
27
- if (claimRandomness[claimId].requested) revert ClaimAlreadyRequested();
28
- claimRandomness[claimId].requested = true;
29
- ID20VRF rng = ID20VRF(vrfCoordinator);
30
- // The same-transaction quote is exact; only off-chain senders need a buffer.
31
- requestId = rng.requestRandomness{value: rng.quoteFee(callbackGasLimit)}(
32
- keccak256(abi.encode(claimId, lockedWork)), callbackGasLimit, refundAddress
33
- );
34
- claimRandomness[claimId].requestId = requestId;
35
- requestToClaim[requestId] = claimId;
36
- knownRequest[requestId] = true;
37
- emit ClaimRandomnessRequested(claimId, requestId);
38
- }
39
-
40
- function _fulfillRandomness(uint256 requestId, bytes32 randomness) internal override {
41
- if (!knownRequest[requestId]) revert UnexpectedCallback();
42
- uint256 claimId = requestToClaim[requestId];
43
- ClaimRandomness storage claim = claimRandomness[claimId];
44
- if (claim.ready) revert UnexpectedCallback();
45
- claim.randomness = randomness;
46
- claim.ready = true;
47
- emit ClaimReady(claimId, requestId, randomness);
48
- }
49
-
50
- function candidateSeed(uint256 claimId, uint8 candidate) public view returns (bytes32) {
51
- ClaimRandomness storage claim = claimRandomness[claimId];
52
- if (!claim.ready) revert ClaimNotReady();
53
- if (candidate >= 3) revert InvalidCandidate();
54
- return keccak256(abi.encode(
55
- keccak256("VRF_D20DAO_CARD_V1"), block.chainid, address(this),
56
- claimId, claim.requestId, claim.randomness, candidate
57
- ));
58
- }
59
- }