@@ -6,6 +6,7 @@ import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
6
6
import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol " ;
7
7
import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol " ;
8
8
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol " ;
9
+ import "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
9
10
import "./EntropyState.sol " ;
10
11
11
12
// Entropy implements a secure 2-party random number generation procedure. The protocol
@@ -74,10 +75,26 @@ import "./EntropyState.sol";
74
75
// cases where the user chooses not to reveal.
75
76
contract Entropy is IEntropy , EntropyState {
76
77
// TODO: Use an upgradeable proxy
77
- constructor (uint pythFeeInWei , address defaultProvider ) {
78
+ constructor (
79
+ uint128 pythFeeInWei ,
80
+ address defaultProvider ,
81
+ bool prefillRequestStorage
82
+ ) {
78
83
_state.accruedPythFeesInWei = 0 ;
79
84
_state.pythFeeInWei = pythFeeInWei;
80
85
_state.defaultProvider = defaultProvider;
86
+
87
+ if (prefillRequestStorage) {
88
+ // Write some data to every storage slot in the requests array such that new requests
89
+ // use a more consistent amount of gas.
90
+ // Note that these requests are not live because their sequenceNumber is 0.
91
+ for (uint8 i = 0 ; i < NUM_REQUESTS; i++ ) {
92
+ EntropyStructs.Request storage req = _state.requests[i];
93
+ req.provider = address (1 );
94
+ req.blockNumber = 1234 ;
95
+ req.commitment = hex "0123 " ;
96
+ }
97
+ }
81
98
}
82
99
83
100
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
@@ -86,7 +103,7 @@ contract Entropy is IEntropy, EntropyState {
86
103
//
87
104
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
88
105
function register (
89
- uint feeInWei ,
106
+ uint128 feeInWei ,
90
107
bytes32 commitment ,
91
108
bytes calldata commitmentMetadata ,
92
109
uint64 chainLength ,
@@ -121,7 +138,7 @@ contract Entropy is IEntropy, EntropyState {
121
138
// Withdraw a portion of the accumulated fees for the provider msg.sender.
122
139
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
123
140
// balance of fees in the contract).
124
- function withdraw (uint256 amount ) public override {
141
+ function withdraw (uint128 amount ) public override {
125
142
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
126
143
msg .sender
127
144
];
@@ -166,24 +183,33 @@ contract Entropy is IEntropy, EntropyState {
166
183
providerInfo.sequenceNumber += 1 ;
167
184
168
185
// Check that fees were paid and increment the pyth / provider balances.
169
- uint requiredFee = getFee (provider);
186
+ uint128 requiredFee = getFee (provider);
170
187
if (msg .value < requiredFee) revert EntropyErrors.InsufficientFee ();
171
188
providerInfo.accruedFeesInWei += providerInfo.feeInWei;
172
- _state.accruedPythFeesInWei += (msg .value - providerInfo.feeInWei);
189
+ _state.accruedPythFeesInWei += (SafeCast.toUint128 (msg .value ) -
190
+ providerInfo.feeInWei);
173
191
174
192
// Store the user's commitment so that we can fulfill the request later.
175
- EntropyStructs.Request storage req = _state.requests[
176
- requestKey (provider, assignedSequenceNumber)
177
- ];
193
+ // Warning: this code needs to overwrite *every* field in the request, because the returned request can be
194
+ // filled with arbitrary data.
195
+ EntropyStructs.Request storage req = allocRequest (
196
+ provider,
197
+ assignedSequenceNumber
198
+ );
178
199
req.provider = provider;
179
200
req.sequenceNumber = assignedSequenceNumber;
180
- req.userCommitment = userCommitment;
181
- req.providerCommitment = providerInfo.currentCommitment;
182
- req.providerCommitmentSequenceNumber = providerInfo
183
- .currentCommitmentSequenceNumber;
201
+ req.numHashes = SafeCast.toUint32 (
202
+ assignedSequenceNumber -
203
+ providerInfo.currentCommitmentSequenceNumber
204
+ );
205
+ req.commitment = keccak256 (
206
+ bytes .concat (userCommitment, providerInfo.currentCommitment)
207
+ );
184
208
185
209
if (useBlockHash) {
186
- req.blockNumber = block .number ;
210
+ req.blockNumber = SafeCast.toUint96 (block .number );
211
+ } else {
212
+ req.blockNumber = 0 ;
187
213
}
188
214
189
215
emit Requested (req);
@@ -202,24 +228,24 @@ contract Entropy is IEntropy, EntropyState {
202
228
bytes32 userRandomness ,
203
229
bytes32 providerRevelation
204
230
) public override returns (bytes32 randomNumber ) {
205
- // TODO: do we need to check that this request exists?
206
231
// TODO: this method may need to be authenticated to prevent griefing
207
- bytes32 key = requestKey (provider, sequenceNumber);
208
- EntropyStructs.Request storage req = _state.requests[key];
209
- // This invariant should be guaranteed to hold by the key construction procedure above, but check it
210
- // explicitly to be extra cautious.
211
- if (req.sequenceNumber != sequenceNumber)
212
- revert EntropyErrors.AssertionFailure ();
213
-
214
- bool valid = isProofValid (
215
- req.providerCommitmentSequenceNumber,
216
- req.providerCommitment,
217
- sequenceNumber,
232
+ EntropyStructs.Request storage req = findRequest (
233
+ provider,
234
+ sequenceNumber
235
+ );
236
+ // Check that there is a request for the given provider / sequence number.
237
+ if (req.provider != provider || req.sequenceNumber != sequenceNumber)
238
+ revert EntropyErrors.NoSuchRequest ();
239
+
240
+ bytes32 providerCommitment = constructProviderCommitment (
241
+ req.numHashes,
218
242
providerRevelation
219
243
);
220
- if (! valid) revert EntropyErrors.IncorrectProviderRevelation ();
221
- if (constructUserCommitment (userRandomness) != req.userCommitment)
222
- revert EntropyErrors.IncorrectUserRevelation ();
244
+ bytes32 userCommitment = constructUserCommitment (userRandomness);
245
+ if (
246
+ keccak256 (bytes .concat (userCommitment, providerCommitment)) !=
247
+ req.commitment
248
+ ) revert EntropyErrors.IncorrectRevelation ();
223
249
224
250
bytes32 blockHash = bytes32 (uint256 (0 ));
225
251
if (req.blockNumber != 0 ) {
@@ -240,7 +266,7 @@ contract Entropy is IEntropy, EntropyState {
240
266
randomNumber
241
267
);
242
268
243
- delete _state.requests[key] ;
269
+ clearRequest (provider, sequenceNumber) ;
244
270
245
271
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
246
272
provider
@@ -270,21 +296,20 @@ contract Entropy is IEntropy, EntropyState {
270
296
address provider ,
271
297
uint64 sequenceNumber
272
298
) public view override returns (EntropyStructs.Request memory req ) {
273
- bytes32 key = requestKey (provider, sequenceNumber);
274
- req = _state.requests[key];
299
+ req = findRequest (provider, sequenceNumber);
275
300
}
276
301
277
302
function getFee (
278
303
address provider
279
- ) public view override returns (uint feeAmount ) {
304
+ ) public view override returns (uint128 feeAmount ) {
280
305
return _state.providers[provider].feeInWei + _state.pythFeeInWei;
281
306
}
282
307
283
308
function getAccruedPythFees ()
284
309
public
285
310
view
286
311
override
287
- returns (uint accruedPythFeesInWei )
312
+ returns (uint128 accruedPythFeesInWei )
288
313
{
289
314
return _state.accruedPythFeesInWei;
290
315
}
@@ -305,31 +330,90 @@ contract Entropy is IEntropy, EntropyState {
305
330
);
306
331
}
307
332
308
- // Create a unique key for an in-flight randomness request (to store it in the contract state)
333
+ // Create a unique key for an in-flight randomness request. Returns both a long key for use in the requestsOverflow
334
+ // mapping and a short key for use in the requests array.
309
335
function requestKey (
310
336
address provider ,
311
337
uint64 sequenceNumber
312
- ) internal pure returns (bytes32 hash ) {
338
+ ) internal pure returns (bytes32 hash , uint8 shortHash ) {
313
339
hash = keccak256 (abi.encodePacked (provider, sequenceNumber));
340
+ shortHash = uint8 (hash[0 ] & NUM_REQUESTS_MASK);
314
341
}
315
342
316
- // Validate that revelation at sequenceNumber is the correct value in the hash chain for a provider whose
317
- // last known revealed random number was lastRevelation at lastSequenceNumber.
318
- function isProofValid (
319
- uint64 lastSequenceNumber ,
320
- bytes32 lastRevelation ,
321
- uint64 sequenceNumber ,
343
+ // Construct a provider's commitment given their revealed random number and the distance in the hash chain
344
+ // between the commitment and the revealed random number.
345
+ function constructProviderCommitment (
346
+ uint64 numHashes ,
322
347
bytes32 revelation
323
- ) internal pure returns (bool valid ) {
324
- if (sequenceNumber <= lastSequenceNumber)
325
- revert EntropyErrors.AssertionFailure ();
326
-
327
- bytes32 currentHash = revelation;
328
- while (sequenceNumber > lastSequenceNumber) {
348
+ ) internal pure returns (bytes32 currentHash ) {
349
+ currentHash = revelation;
350
+ while (numHashes > 0 ) {
329
351
currentHash = keccak256 (bytes .concat (currentHash));
330
- sequenceNumber -= 1 ;
352
+ numHashes -= 1 ;
331
353
}
354
+ }
355
+
356
+ // Find an in-flight request.
357
+ // Note that this method can return requests that are not currently active. The caller is responsible for checking
358
+ // that the returned request is active (if they care).
359
+ function findRequest (
360
+ address provider ,
361
+ uint64 sequenceNumber
362
+ ) internal view returns (EntropyStructs.Request storage req ) {
363
+ (bytes32 key , uint8 shortKey ) = requestKey (provider, sequenceNumber);
364
+
365
+ req = _state.requests[shortKey];
366
+ if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
367
+ return req;
368
+ } else {
369
+ req = _state.requestsOverflow[key];
370
+ }
371
+ }
372
+
373
+ // Clear the storage for an in-flight request, deleting it from the hash table.
374
+ function clearRequest (address provider , uint64 sequenceNumber ) internal {
375
+ (bytes32 key , uint8 shortKey ) = requestKey (provider, sequenceNumber);
376
+
377
+ EntropyStructs.Request storage req = _state.requests[shortKey];
378
+ if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
379
+ req.sequenceNumber = 0 ;
380
+ } else {
381
+ delete _state.requestsOverflow[key];
382
+ }
383
+ }
384
+
385
+ // Allocate storage space for a new in-flight request. This method returns a pointer to a storage slot
386
+ // that the caller should overwrite with the new request. Note that the memory at this storage slot may
387
+ // -- and will -- be filled with arbitrary values, so the caller *must* overwrite every field of the returned
388
+ // struct.
389
+ function allocRequest (
390
+ address provider ,
391
+ uint64 sequenceNumber
392
+ ) internal returns (EntropyStructs.Request storage req ) {
393
+ (, uint8 shortKey ) = requestKey (provider, sequenceNumber);
394
+
395
+ req = _state.requests[shortKey];
396
+ if (isActive (req)) {
397
+ // There's already a prior active request in the storage slot we want to use.
398
+ // Overflow the prior request to the requestsOverflow mapping.
399
+ // It is important that this code overflows the *prior* request to the mapping, and not the new request.
400
+ // There is a chance that some requests never get revealed and remain active forever. We do not want such
401
+ // requests to fill up all of the space in the array and cause all new requests to incur the higher gas cost
402
+ // of the mapping.
403
+ //
404
+ // This operation is expensive, but should be rare. If overflow happens frequently, increase
405
+ // the size of the requests array to support more concurrent active requests.
406
+ (bytes32 reqKey , ) = requestKey (req.provider, req.sequenceNumber);
407
+ _state.requestsOverflow[reqKey] = req;
408
+ }
409
+ }
332
410
333
- valid = currentHash == lastRevelation;
411
+ // Returns true if a request is active, i.e., its corresponding random value has not yet been revealed.
412
+ function isActive (
413
+ EntropyStructs.Request storage req
414
+ ) internal view returns (bool ) {
415
+ // Note that a provider's initial registration occupies sequence number 0, so there is no way to construct
416
+ // a randomness request with sequence number 0.
417
+ return req.sequenceNumber != 0 ;
334
418
}
335
419
}
0 commit comments