-
Notifications
You must be signed in to change notification settings - Fork 247
/
Copy pathEntropy.sol
895 lines (800 loc) · 36.9 KB
/
Entropy.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.0;
import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
import "@pythnetwork/entropy-sdk-solidity/EntropyErrors.sol";
import "@pythnetwork/entropy-sdk-solidity/EntropyEvents.sol";
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol";
import "@openzeppelin/contracts/utils/math/SafeCast.sol";
import "ExcessivelySafeCall/ExcessivelySafeCall.sol";
import "./EntropyState.sol";
// Entropy implements a secure 2-party random number generation procedure. The protocol
// is an extension of a simple commit/reveal protocol. The original version has the following steps:
//
// 1. Two parties A and B each draw a random number x_{A,B}
// 2. A and B then share h_{A,B} = hash(x_{A,B})
// 3. A and B reveal x_{A,B}
// 4. Both parties verify that hash(x_{A, B}) == h_{A,B}
// 5. The random number r = hash(x_A, x_B)
//
// This protocol has the property that the result is random as long as either A or B are honest.
// Thus, neither party needs to trust the other -- as long as they are themselves honest, they can
// ensure that the result r is random.
//
// Entropy implements a version of this protocol that is optimized for on-chain usage. The
// key difference is that one of the participants (the provider) commits to a sequence of random numbers
// up-front using a hash chain. Users of the protocol then simply grab the next random number in the sequence.
//
// Setup: The provider P computes a sequence of N random numbers, x_i (i = 0...N-1):
// x_{N-1} = random()
// x_i = hash(x_{i + 1})
// The provider commits to x_0 by posting it to the contract. Each random number in the sequence can then be
// verified against the previous one in the sequence by hashing it, i.e., hash(x_i) == x_{i - 1}
//
// Request: To produce a random number, the following steps occur.
// 1. The user draws a random number x_U, and submits h_U = hash(x_U) to this contract
// 2. The contract remembers h_U and assigns it an incrementing sequence number i, representing which
// of the provider's random numbers the user will receive.
// 3. The user submits an off-chain request (e.g. via HTTP) to the provider to reveal the i'th random number.
// 4. The provider checks the on-chain sequence number and ensures it is > i. If it is not, the provider
// refuses to reveal the ith random number. The provider should wait for a sufficient number of block confirmations
// to ensure that the request does not get re-orged out of the blockchain.
// 5. The provider reveals x_i to the user.
// 6. The user submits both the provider's revealed number x_i and their own x_U to the contract.
// 7. The contract verifies hash(x_i) == x_{i-1} to prove that x_i is the i'th random number. The contract also checks that hash(x_U) == h_U.
// The contract stores x_i as the i'th random number to reuse for future verifications.
// 8. If both of the above conditions are satisfied, the random number r = hash(x_i, x_U).
// (Optional) as an added security mechanism, this step can further incorporate the blockhash of the block that the
// request transaction landed in: r = hash(x_i, x_U, blockhash).
//
// This protocol has the same security properties as the 2-party randomness protocol above: as long as either
// the provider or user is honest, the number r is random. Honesty here means that the participant keeps their
// random number x a secret until the revelation phase (step 5) of the protocol. Note that providers need to
// be careful to ensure their off-chain service isn't compromised to reveal the random numbers -- if this occurs,
// then users will be able to influence the random number r.
//
// The Entropy implementation of the above protocol allows anyone to permissionlessly register to be a
// randomness provider. Users then choose which provider to request randomness from. Each provider can set
// their own fee for the service. In addition, the Entropy contract charges a flat fee that goes to the
// Pyth protocol for each requested random number. Fees are paid in the native token of the network.
//
// This implementation has two intricacies that merit further explanation. First, the implementation supports
// multiple concurrent requests for randomness by checking the provider's random number against their last known
// random number. Verification therefore may require computing multiple hashes (~ the number of concurrent requests).
// Second, the implementation allows providers to rotate their commitment at any time. This operation allows
// providers to commit to additional random numbers once they reach the end of their initial sequence, or rotate out
// a compromised sequence. On rotation, any in-flight requests continue to use the pre-rotation commitment.
// Providers can use the sequence number of the request along with the event log of their registrations to determine
// which hash chain contains the requested random number.
//
// Warning to integrators:
// An important caveat of this protocol is that the user can compute the random number r before
// revealing their own number to the contract. This property means that the user can choose to halt the
// protocol prior to the random number being revealed (i.e., prior to step (6) above). Integrators should ensure that
// the user is always incentivized to reveal their random number, and that the protocol has an escape hatch for
// cases where the user chooses not to reveal.
abstract contract Entropy is IEntropy, EntropyState {
using ExcessivelySafeCall for address;
function _initialize(
address admin,
uint128 pythFeeInWei,
address defaultProvider,
bool prefillRequestStorage
) internal {
require(admin != address(0), "admin is zero address");
require(
defaultProvider != address(0),
"defaultProvider is zero address"
);
_state.admin = admin;
_state.accruedPythFeesInWei = 0;
_state.pythFeeInWei = pythFeeInWei;
_state.defaultProvider = defaultProvider;
if (prefillRequestStorage) {
// Write some data to every storage slot in the requests array such that new requests
// use a more consistent amount of gas.
// Note that these requests are not live because their sequenceNumber is 0.
for (uint8 i = 0; i < NUM_REQUESTS; i++) {
EntropyStructs.Request storage req = _state.requests[i];
req.provider = address(1);
req.blockNumberOrGasLimit = 1234;
req.commitment = hex"0123";
}
}
}
// Register msg.sender as a randomness provider. The arguments are the provider's configuration parameters
// and initial commitment. Re-registering the same provider rotates the provider's commitment (and updates
// the feeInWei).
//
// chainLength is the number of values in the hash chain *including* the commitment, that is, chainLength >= 1.
function register(
uint128 feeInWei,
bytes32 commitment,
bytes calldata commitmentMetadata,
uint64 chainLength,
bytes calldata uri
) public override {
if (chainLength == 0) revert EntropyErrors.AssertionFailure();
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
// NOTE: this method implementation depends on the fact that ProviderInfo will be initialized to all-zero.
// Specifically, accruedFeesInWei is intentionally not set. On initial registration, it will be zero,
// then on future registrations, it will be unchanged. Similarly, provider.sequenceNumber defaults to 0
// on initial registration.
provider.feeInWei = feeInWei;
provider.originalCommitment = commitment;
provider.originalCommitmentSequenceNumber = provider.sequenceNumber;
provider.currentCommitment = commitment;
provider.currentCommitmentSequenceNumber = provider.sequenceNumber;
provider.commitmentMetadata = commitmentMetadata;
provider.endSequenceNumber = provider.sequenceNumber + chainLength;
provider.uri = uri;
provider.sequenceNumber += 1;
emit Registered(provider);
}
// Withdraw a portion of the accumulated fees for the provider msg.sender.
// Calling this function will transfer `amount` wei to the caller (provided that they have accrued a sufficient
// balance of fees in the contract).
function withdraw(uint128 amount) public override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
msg.sender
];
// Use checks-effects-interactions pattern to prevent reentrancy attacks.
require(
providerInfo.accruedFeesInWei >= amount,
"Insufficient balance"
);
providerInfo.accruedFeesInWei -= amount;
// Interaction with an external contract or token transfer
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "withdrawal to msg.sender failed");
emit Withdrawal(msg.sender, msg.sender, amount);
}
function withdrawAsFeeManager(
address provider,
uint128 amount
) external override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (providerInfo.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
if (providerInfo.feeManager != msg.sender) {
revert EntropyErrors.Unauthorized();
}
// Use checks-effects-interactions pattern to prevent reentrancy attacks.
require(
providerInfo.accruedFeesInWei >= amount,
"Insufficient balance"
);
providerInfo.accruedFeesInWei -= amount;
// Interaction with an external contract or token transfer
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "withdrawal to msg.sender failed");
emit Withdrawal(provider, msg.sender, amount);
}
// requestHelper allocates and returns a new request for the given provider.
// Note: This method will revert unless the caller provides a sufficient fee
// (at least getFee(provider)) as msg.value.
function requestHelper(
address provider,
bytes32 userCommitment,
bool useBlockhash,
bool isRequestWithCallback,
uint64 callbackGasLimit
) internal returns (EntropyStructs.Request storage req) {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (_state.providers[provider].sequenceNumber == 0)
revert EntropyErrors.NoSuchProvider();
// Assign a sequence number to the request
uint64 assignedSequenceNumber = providerInfo.sequenceNumber;
if (assignedSequenceNumber >= providerInfo.endSequenceNumber)
revert EntropyErrors.OutOfRandomness();
providerInfo.sequenceNumber += 1;
// Check that fees were paid and increment the pyth / provider balances.
uint128 requiredFee = getFeeForGas(provider, callbackGasLimit);
if (msg.value < requiredFee) revert EntropyErrors.InsufficientFee();
uint128 providerFee = getProviderFee(provider, callbackGasLimit);
providerInfo.accruedFeesInWei += providerFee;
_state.accruedPythFeesInWei += (SafeCast.toUint128(msg.value) -
providerFee);
// Store the user's commitment so that we can fulfill the request later.
// Warning: this code needs to overwrite *every* field in the request, because the returned request can be
// filled with arbitrary data.
req = allocRequest(provider, assignedSequenceNumber);
req.provider = provider;
req.sequenceNumber = assignedSequenceNumber;
req.numHashes = SafeCast.toUint32(
assignedSequenceNumber -
providerInfo.currentCommitmentSequenceNumber
);
if (
providerInfo.maxNumHashes != 0 &&
req.numHashes > providerInfo.maxNumHashes
) {
revert EntropyErrors.LastRevealedTooOld();
}
req.commitment = keccak256(
bytes.concat(userCommitment, providerInfo.currentCommitment)
);
req.requester = msg.sender;
if (useBlockhash && isRequestWithCallback) {
revert EntropyErrors.AssertionFailure();
} else if (isRequestWithCallback) {
req.isRequestWithCallback = isRequestWithCallback;
if (callbackGasLimit == 0) {
req.blockNumberOrGasLimit = providerInfo.defaultGasLimit;
} else {
req.blockNumberOrGasLimit = callbackGasLimit;
}
req.useBlockhash = false;
} else {
req.isRequestWithCallback = false;
req.blockNumberOrGasLimit = SafeCast.toUint64(block.number);
req.useBlockhash = useBlockhash;
}
req.callbackFailed = false;
req.reentryGuard = false;
}
// As a user, request a random number from `provider`. Prior to calling this method, the user should
// generate a random number x and keep it secret. The user should then compute hash(x) and pass that
// as the userCommitment argument. (You may call the constructUserCommitment method to compute the hash.)
//
// This method returns a sequence number. The user should pass this sequence number to
// their chosen provider (the exact method for doing so will depend on the provider) to retrieve the provider's
// number. The user should then call fulfillRequest to construct the final random number.
//
// This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
// Note that excess value is *not* refunded to the caller.
function request(
address provider,
bytes32 userCommitment,
bool useBlockHash
) public payable override returns (uint64 assignedSequenceNumber) {
EntropyStructs.Request storage req = requestHelper(
provider,
userCommitment,
useBlockHash,
false,
0
);
assignedSequenceNumber = req.sequenceNumber;
emit Requested(req);
}
// Request a random number. The method expects the provider address and a secret random number
// in the arguments. It returns a sequence number.
//
// The address calling this function should be a contract that inherits from the IEntropyConsumer interface.
// The `entropyCallback` method on that interface will receive a callback with the generated random number.
//
// This method will revert unless the caller provides a sufficient fee (at least getFee(provider)) as msg.value.
// Note that excess value is *not* refunded to the caller.
function requestWithCallback(
address provider,
bytes32 userRandomNumber
) public payable override returns (uint64) {
EntropyStructs.Request storage req = requestHelper(
provider,
constructUserCommitment(userRandomNumber),
// If useBlockHash is set to true, it allows a scenario in which the provider and miner can collude.
// If we remove the blockHash from this, the provider would have no choice but to provide its committed
// random number. Hence, useBlockHash is set to false.
false,
true,
0
);
emit RequestedWithCallback(
provider,
req.requester,
req.sequenceNumber,
userRandomNumber,
req
);
return req.sequenceNumber;
}
function requestWithCallbackAndGas(
address provider,
bytes32 userRandomNumber,
uint64 gasLimit
) public payable override returns (uint64) {
EntropyStructs.Request storage req = requestHelper(
provider,
constructUserCommitment(userRandomNumber),
// If useBlockHash is set to true, it allows a scenario in which the provider and miner can collude.
// If we remove the blockHash from this, the provider would have no choice but to provide its committed
// random number. Hence, useBlockHash is set to false.
false,
true,
gasLimit
);
emit RequestedWithCallback(
provider,
req.requester,
req.sequenceNumber,
userRandomNumber,
req
);
return req.sequenceNumber;
}
// This method validates the provided user's revelation and provider's revelation against the corresponding
// commitment in the in-flight request. If both values are validated, this method will update the provider
// current commitment and returns the generated random number.
function revealHelper(
EntropyStructs.Request storage req,
bytes32 userRevelation,
bytes32 providerRevelation
) internal returns (bytes32 randomNumber, bytes32 blockHash) {
bytes32 providerCommitment = constructProviderCommitment(
req.numHashes,
providerRevelation
);
bytes32 userCommitment = constructUserCommitment(userRevelation);
if (
keccak256(bytes.concat(userCommitment, providerCommitment)) !=
req.commitment
) revert EntropyErrors.IncorrectRevelation();
blockHash = bytes32(uint256(0));
if (req.useBlockhash) {
bytes32 _blockHash = blockhash(req.blockNumberOrGasLimit);
// The `blockhash` function will return zero if the req.blockNumberOrGasLimit is equal to the current
// block number, or if it is not within the 256 most recent blocks. This allows the user to
// select between two random numbers by executing the reveal function in the same block as the
// request, or after 256 blocks. This gives each user two chances to get a favorable result on
// each request.
// Revert this transaction for when the blockHash is 0;
if (_blockHash == bytes32(uint256(0)))
revert EntropyErrors.BlockhashUnavailable();
blockHash = _blockHash;
}
randomNumber = combineRandomValues(
userRevelation,
providerRevelation,
blockHash
);
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
req.provider
];
if (providerInfo.currentCommitmentSequenceNumber < req.sequenceNumber) {
providerInfo.currentCommitmentSequenceNumber = req.sequenceNumber;
providerInfo.currentCommitment = providerRevelation;
}
}
// Advance the provider commitment and increase the sequence number.
// This is used to reduce the `numHashes` required for future requests which leads to reduced gas usage.
function advanceProviderCommitment(
address provider,
uint64 advancedSequenceNumber,
bytes32 providerRevelation
) public override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (
advancedSequenceNumber <=
providerInfo.currentCommitmentSequenceNumber
) revert EntropyErrors.UpdateTooOld();
if (advancedSequenceNumber >= providerInfo.endSequenceNumber)
revert EntropyErrors.AssertionFailure();
uint32 numHashes = SafeCast.toUint32(
advancedSequenceNumber -
providerInfo.currentCommitmentSequenceNumber
);
bytes32 providerCommitment = constructProviderCommitment(
numHashes,
providerRevelation
);
if (providerCommitment != providerInfo.currentCommitment)
revert EntropyErrors.IncorrectRevelation();
providerInfo.currentCommitmentSequenceNumber = advancedSequenceNumber;
providerInfo.currentCommitment = providerRevelation;
if (
providerInfo.currentCommitmentSequenceNumber >=
providerInfo.sequenceNumber
) {
// This means the provider called the function with a sequence number that was not yet requested.
// Providers should never do this and we consider such an implementation flawed.
// Assuming this is landed on-chain it's better to bump the sequence number and never use that range
// for future requests. Otherwise, someone can use the leaked revelation to derive favorable random numbers.
providerInfo.sequenceNumber =
providerInfo.currentCommitmentSequenceNumber +
1;
}
}
// Fulfill a request for a random number. This method validates the provided userRandomness and provider's proof
// against the corresponding commitments in the in-flight request. If both values are validated, this method returns
// the corresponding random number.
//
// Note that this function can only be called once per in-flight request. Calling this function deletes the stored
// request information (so that the contract doesn't use a linear amount of storage in the number of requests).
// If you need to use the returned random number more than once, you are responsible for storing it.
//
// This function must be called by the same `msg.sender` that originally requested the random number. This check
// prevents denial-of-service attacks where another actor front-runs the requester's reveal transaction.
function reveal(
address provider,
uint64 sequenceNumber,
bytes32 userRevelation,
bytes32 providerRevelation
) public override returns (bytes32 randomNumber) {
EntropyStructs.Request storage req = findActiveRequest(
provider,
sequenceNumber
);
if (req.isRequestWithCallback) {
revert EntropyErrors.InvalidRevealCall();
}
if (req.requester != msg.sender) {
revert EntropyErrors.Unauthorized();
}
bytes32 blockHash;
(randomNumber, blockHash) = revealHelper(
req,
userRevelation,
providerRevelation
);
emit Revealed(
req,
userRevelation,
providerRevelation,
blockHash,
randomNumber
);
clearRequest(provider, sequenceNumber);
}
// Fulfill a request for a random number. This method validates the provided userRandomness
// and provider's revelation against the corresponding commitment in the in-flight request. If both values are validated
// and the requestor address is a contract address, this function calls the requester's entropyCallback method with the
// sequence number, provider address and the random number as arguments. Else if the requestor is an EOA, it won't call it.
//
// Note that this function can only be called once per in-flight request. Calling this function deletes the stored
// request information (so that the contract doesn't use a linear amount of storage in the number of requests).
// If you need to use the returned random number more than once, you are responsible for storing it.
//
// Anyone can call this method to fulfill a request, but the callback will only be made to the original requester.
function revealWithCallback(
address provider,
uint64 sequenceNumber,
bytes32 userRandomNumber,
bytes32 providerRevelation
) public override {
EntropyStructs.Request storage req = findActiveRequest(
provider,
sequenceNumber
);
if (!req.isRequestWithCallback) {
revert EntropyErrors.InvalidRevealCall();
}
// Invariant check: all callback requests should have useBlockhash set to false.
if (req.useBlockhash) {
revert EntropyErrors.AssertionFailure();
}
if (req.reentryGuard) {
revert EntropyErrors.InvalidRevealCall();
}
bytes32 blockHash;
bytes32 randomNumber;
(randomNumber, blockHash) = revealHelper(
req,
userRandomNumber,
providerRevelation
);
address callAddress = req.requester;
// blockNumberOrGasLimit holds the gas limit in the callback case.
// If the gas limit is 0, then the provider hasn't configured their default limit,
// so we default to the prior entropy flow (where there is no failure state).
// Similarly, if the request has already failed, we fall back to the prior flow so that
// recovery attempts can provide more gas / directly see the revert reason.
if (req.blockNumberOrGasLimit != 0 && !req.callbackFailed) {
// TODO: need to validate that we have enough gas left to forward (?)
// Or at least that we forwarded enough gas before marking the callback as failed
/*
if (gasleft() < req.blockNumberOrGasLimit) {
}
*/
req.reentryGuard = true;
bool success;
bytes memory ret;
(success, ret) = callAddress.excessivelySafeCall(
req.blockNumberOrGasLimit,
0,
256,
abi.encodeWithSelector(
IEntropyConsumer._entropyCallback.selector,
sequenceNumber,
provider,
randomNumber
)
);
req.reentryGuard = false;
if (success) {
emit RevealedWithCallback(
req,
userRandomNumber,
providerRevelation,
randomNumber
);
clearRequest(provider, sequenceNumber);
} else {
emit CallbackFailed(
provider,
req.requester,
sequenceNumber,
ret
);
req.callbackFailed = true;
}
} else {
emit RevealedWithCallback(
req,
userRandomNumber,
providerRevelation,
randomNumber
);
clearRequest(provider, sequenceNumber);
// Check if the callAddress is a contract account.
uint len;
assembly {
len := extcodesize(callAddress)
}
if (len != 0) {
req.reentryGuard = true;
IEntropyConsumer(callAddress)._entropyCallback(
sequenceNumber,
provider,
randomNumber
);
req.reentryGuard = false;
}
}
}
function getProviderInfo(
address provider
) public view override returns (EntropyStructs.ProviderInfo memory info) {
info = _state.providers[provider];
}
function getDefaultProvider()
public
view
override
returns (address provider)
{
provider = _state.defaultProvider;
}
function getRequest(
address provider,
uint64 sequenceNumber
) public view override returns (EntropyStructs.Request memory req) {
req = findRequest(provider, sequenceNumber);
}
function getFee(
address provider
) public view override returns (uint128 feeAmount) {
return getFeeForGas(provider, 0);
}
function getFeeForGas(
address provider,
uint64 gasLimit
) public view override returns (uint128 feeAmount) {
return getProviderFee(provider, gasLimit) + _state.pythFeeInWei;
}
function getProviderFee(
address providerAddr,
uint64 gasLimit
) internal view returns (uint128 feeAmount) {
EntropyStructs.ProviderInfo memory provider = _state.providers[
providerAddr
];
if (gasLimit > provider.defaultGasLimit) {
// This calculation rounds down the fee, which means that users can get some gas in the callback for free.
// However, the value of the free gas is < 1 wei, which is insignificant.
uint128 additionalFee = ((gasLimit - provider.defaultGasLimit) *
provider.feeInWei) / provider.defaultGasLimit;
return provider.feeInWei + additionalFee;
} else {
return provider.feeInWei;
}
}
function getPythFee() public view returns (uint128 feeAmount) {
return _state.pythFeeInWei;
}
function getAccruedPythFees()
public
view
override
returns (uint128 accruedPythFeesInWei)
{
return _state.accruedPythFeesInWei;
}
// Set provider fee. It will revert if provider is not registered.
function setProviderFee(uint128 newFeeInWei) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
uint128 oldFeeInWei = provider.feeInWei;
provider.feeInWei = newFeeInWei;
emit ProviderFeeUpdated(msg.sender, oldFeeInWei, newFeeInWei);
}
function setProviderFeeAsFeeManager(
address provider,
uint128 newFeeInWei
) external override {
EntropyStructs.ProviderInfo storage providerInfo = _state.providers[
provider
];
if (providerInfo.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
if (providerInfo.feeManager != msg.sender) {
revert EntropyErrors.Unauthorized();
}
uint128 oldFeeInWei = providerInfo.feeInWei;
providerInfo.feeInWei = newFeeInWei;
emit ProviderFeeUpdated(provider, oldFeeInWei, newFeeInWei);
}
// Set provider uri. It will revert if provider is not registered.
function setProviderUri(bytes calldata newUri) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
bytes memory oldUri = provider.uri;
provider.uri = newUri;
emit ProviderUriUpdated(msg.sender, oldUri, newUri);
}
function setFeeManager(address manager) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
address oldFeeManager = provider.feeManager;
provider.feeManager = manager;
emit ProviderFeeManagerUpdated(msg.sender, oldFeeManager, manager);
}
// Set the maximum number of hashes to record in a request. This should be set according to the maximum gas limit
// the provider supports for callbacks.
function setMaxNumHashes(uint32 maxNumHashes) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
uint32 oldMaxNumHashes = provider.maxNumHashes;
provider.maxNumHashes = maxNumHashes;
emit ProviderMaxNumHashesAdvanced(
msg.sender,
oldMaxNumHashes,
maxNumHashes
);
}
// Set the default gas limit for a request.
function setDefaultGasLimit(uint64 gasLimit) external override {
EntropyStructs.ProviderInfo storage provider = _state.providers[
msg.sender
];
if (provider.sequenceNumber == 0) {
revert EntropyErrors.NoSuchProvider();
}
uint64 oldGasLimit = provider.defaultGasLimit;
provider.defaultGasLimit = gasLimit;
emit ProviderDefaultGasLimitUpdated(msg.sender, oldGasLimit, gasLimit);
}
function constructUserCommitment(
bytes32 userRandomness
) public pure override returns (bytes32 userCommitment) {
userCommitment = keccak256(bytes.concat(userRandomness));
}
function combineRandomValues(
bytes32 userRandomness,
bytes32 providerRandomness,
bytes32 blockHash
) public pure override returns (bytes32 combinedRandomness) {
combinedRandomness = keccak256(
abi.encodePacked(userRandomness, providerRandomness, blockHash)
);
}
// Create a unique key for an in-flight randomness request. Returns both a long key for use in the requestsOverflow
// mapping and a short key for use in the requests array.
function requestKey(
address provider,
uint64 sequenceNumber
) internal pure returns (bytes32 hash, uint8 shortHash) {
hash = keccak256(abi.encodePacked(provider, sequenceNumber));
shortHash = uint8(hash[0] & NUM_REQUESTS_MASK);
}
// Construct a provider's commitment given their revealed random number and the distance in the hash chain
// between the commitment and the revealed random number.
function constructProviderCommitment(
uint64 numHashes,
bytes32 revelation
) internal pure returns (bytes32 currentHash) {
currentHash = revelation;
while (numHashes > 0) {
currentHash = keccak256(bytes.concat(currentHash));
numHashes -= 1;
}
}
// Find an in-flight active request for given the provider and the sequence number.
// This method returns a reference to the request, and will revert if the request is
// not active.
function findActiveRequest(
address provider,
uint64 sequenceNumber
) internal view returns (EntropyStructs.Request storage req) {
req = findRequest(provider, sequenceNumber);
// Check there is an active request for the given provider and sequence number.
if (
!isActive(req) ||
req.provider != provider ||
req.sequenceNumber != sequenceNumber
) revert EntropyErrors.NoSuchRequest();
}
// Find an in-flight request.
// Note that this method can return requests that are not currently active. The caller is responsible for checking
// that the returned request is active (if they care).
function findRequest(
address provider,
uint64 sequenceNumber
) internal view returns (EntropyStructs.Request storage req) {
(bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
req = _state.requests[shortKey];
if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
return req;
} else {
req = _state.requestsOverflow[key];
}
}
// Clear the storage for an in-flight request, deleting it from the hash table.
function clearRequest(address provider, uint64 sequenceNumber) internal {
(bytes32 key, uint8 shortKey) = requestKey(provider, sequenceNumber);
EntropyStructs.Request storage req = _state.requests[shortKey];
if (req.provider == provider && req.sequenceNumber == sequenceNumber) {
req.sequenceNumber = 0;
} else {
delete _state.requestsOverflow[key];
}
}
// Allocate storage space for a new in-flight request. This method returns a pointer to a storage slot
// that the caller should overwrite with the new request. Note that the memory at this storage slot may
// -- and will -- be filled with arbitrary values, so the caller *must* overwrite every field of the returned
// struct.
function allocRequest(
address provider,
uint64 sequenceNumber
) internal returns (EntropyStructs.Request storage req) {
(, uint8 shortKey) = requestKey(provider, sequenceNumber);
req = _state.requests[shortKey];
if (isActive(req)) {
// There's already a prior active request in the storage slot we want to use.
// Overflow the prior request to the requestsOverflow mapping.
// It is important that this code overflows the *prior* request to the mapping, and not the new request.
// There is a chance that some requests never get revealed and remain active forever. We do not want such
// requests to fill up all of the space in the array and cause all new requests to incur the higher gas cost
// of the mapping.
//
// This operation is expensive, but should be rare. If overflow happens frequently, increase
// the size of the requests array to support more concurrent active requests.
(bytes32 reqKey, ) = requestKey(req.provider, req.sequenceNumber);
_state.requestsOverflow[reqKey] = req;
}
}
// Returns true if a request is active, i.e., its corresponding random value has not yet been revealed.
function isActive(
EntropyStructs.Request storage req
) internal view returns (bool) {
// Note that a provider's initial registration occupies sequence number 0, so there is no way to construct
// a randomness request with sequence number 0.
return req.sequenceNumber != 0;
}
}