Explorar el Código

feat(entropy): Add callback failure states to the contract (#2546)

Currently, the Entropy keeper cannot invoke a callback if the underlying callback fails. This creates confusion for users, who can't distinguish between "the keeper is offline" and "my callback code reverts".

This change creates a failure flow for callbacks that allows the keeper's transaction to succeed while explicitly reporting a failure of the underlying callback. Requests with failing callbacks remain on the blockchain and can be re-invoked by users. This feature allows users to gracefully recover in cases where the callback fails for transient reasons. (In the future, it will also allow recovery in the case where the callback uses too much gas.)

Note that this PR doesn't cover the case where the callback uses too much gas. I will send a follow-up PR for that.
Jayant Krishnamurthy hace 7 meses
padre
commit
f841dfc14e

+ 8 - 0
pnpm-lock.yaml

@@ -2351,6 +2351,9 @@ importers:
       '@matterlabs/hardhat-zksync-solc':
         specifier: ^0.3.14
         version: 0.3.17(encoding@0.1.13)(hardhat@2.22.19(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(typescript@5.8.2)(utf-8-validate@6.0.3))
+      '@nomad-xyz/excessively-safe-call':
+        specifier: ^0.0.1-rc.1
+        version: 0.0.1-rc.1
       '@nomiclabs/hardhat-etherscan':
         specifier: ^3.1.7
         version: 3.1.8(hardhat@2.22.19(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.14.0)(typescript@5.8.2))(typescript@5.8.2)(utf-8-validate@6.0.3))
@@ -6085,6 +6088,9 @@ packages:
     resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
     engines: {node: '>=12.4.0'}
 
+  '@nomad-xyz/excessively-safe-call@0.0.1-rc.1':
+    resolution: {integrity: sha512-Q5GVakBy8J1kWjydH6W5LNbkYY+Cw2doBiLodOfbFGujeng6zM+EtMLb/V+vkWbskbM81y2r+LG5NmxsxyElPA==}
+
   '@nomicfoundation/edr-darwin-arm64@0.8.0':
     resolution: {integrity: sha512-sKTmOu/P5YYhxT0ThN2Pe3hmCE/5Ag6K/eYoiavjLWbR7HEb5ZwPu2rC3DpuUk1H+UKJqt7o4/xIgJxqw9wu6A==}
     engines: {node: '>= 18'}
@@ -28245,6 +28251,8 @@ snapshots:
 
   '@nolyfill/is-core-module@1.0.39': {}
 
+  '@nomad-xyz/excessively-safe-call@0.0.1-rc.1': {}
+
   '@nomicfoundation/edr-darwin-arm64@0.8.0': {}
 
   '@nomicfoundation/edr-darwin-x64@0.8.0': {}

+ 84 - 22
target_chains/ethereum/contracts/contracts/entropy/Entropy.sol

@@ -8,7 +8,9 @@ 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 "@nomad-xyz/excessively-safe-call/src/ExcessivelySafeCall.sol";
 import "./EntropyState.sol";
+import "@pythnetwork/entropy-sdk-solidity/EntropyStatusConstants.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:
@@ -76,6 +78,8 @@ import "./EntropyState.sol";
 // 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,
@@ -247,7 +251,9 @@ abstract contract Entropy is IEntropy, EntropyState {
 
         req.blockNumber = SafeCast.toUint64(block.number);
         req.useBlockhash = useBlockhash;
-        req.isRequestWithCallback = isRequestWithCallback;
+        req.callbackStatus = isRequestWithCallback
+            ? EntropyStatusConstants.CALLBACK_NOT_STARTED
+            : EntropyStatusConstants.CALLBACK_NOT_NECESSARY;
     }
 
     // As a user, request a random number from `provider`. Prior to calling this method, the user should
@@ -403,7 +409,7 @@ abstract contract Entropy is IEntropy, EntropyState {
     }
 
     // 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 function returns
+    // 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
@@ -423,7 +429,9 @@ abstract contract Entropy is IEntropy, EntropyState {
             sequenceNumber
         );
 
-        if (req.isRequestWithCallback) {
+        if (
+            req.callbackStatus != EntropyStatusConstants.CALLBACK_NOT_NECESSARY
+        ) {
             revert EntropyErrors.InvalidRevealCall();
         }
 
@@ -467,9 +475,14 @@ abstract contract Entropy is IEntropy, EntropyState {
             sequenceNumber
         );
 
-        if (!req.isRequestWithCallback) {
+        if (
+            !(req.callbackStatus ==
+                EntropyStatusConstants.CALLBACK_NOT_STARTED ||
+                req.callbackStatus == EntropyStatusConstants.CALLBACK_FAILED)
+        ) {
             revert EntropyErrors.InvalidRevealCall();
         }
+
         bytes32 blockHash;
         bytes32 randomNumber;
         (randomNumber, blockHash) = revealHelper(
@@ -480,26 +493,75 @@ abstract contract Entropy is IEntropy, EntropyState {
 
         address callAddress = req.requester;
 
-        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) {
-            IEntropyConsumer(callAddress)._entropyCallback(
-                sequenceNumber,
-                provider,
+        // Requests that haven't been invoked yet will be invoked safely (catching reverts), and
+        // any reverts will be reported as an event. Any failing requests move to a failure state
+        // at which point they can be recovered. The recovery flow invokes the callback directly
+        // (no catching errors) which allows callers to easily see the revert reason.
+        if (req.callbackStatus == EntropyStatusConstants.CALLBACK_NOT_STARTED) {
+            req.callbackStatus = EntropyStatusConstants.CALLBACK_IN_PROGRESS;
+            bool success;
+            bytes memory ret;
+            (success, ret) = callAddress.excessivelySafeCall(
+                gasleft(), // TODO: providers need to be able to configure this in the future.
+                256, // copy at most 256 bytes of the return value into ret.
+                abi.encodeWithSelector(
+                    IEntropyConsumer._entropyCallback.selector,
+                    sequenceNumber,
+                    provider,
+                    randomNumber
+                )
+            );
+            // Reset status to not started here in case the transaction reverts.
+            req.callbackStatus = EntropyStatusConstants.CALLBACK_NOT_STARTED;
+
+            if (success) {
+                emit RevealedWithCallback(
+                    req,
+                    userRandomNumber,
+                    providerRevelation,
+                    randomNumber
+                );
+                clearRequest(provider, sequenceNumber);
+            } else if (ret.length > 0) {
+                // Callback reverted for some reason that is *not* out-of-gas.
+                emit CallbackFailed(
+                    provider,
+                    req.requester,
+                    sequenceNumber,
+                    userRandomNumber,
+                    providerRevelation,
+                    randomNumber,
+                    ret
+                );
+                req.callbackStatus = EntropyStatusConstants.CALLBACK_FAILED;
+            } else {
+                // The callback ran out of gas
+                // TODO: this case will go away once we add provider gas limits, so we're not putting in a custom error type.
+                require(false, "provider needs to send more gas");
+            }
+        } else {
+            // This case uses the checks-effects-interactions pattern to avoid reentry attacks
+            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) {
+                IEntropyConsumer(callAddress)._entropyCallback(
+                    sequenceNumber,
+                    provider,
+                    randomNumber
+                );
+            }
         }
     }
 

+ 187 - 25
target_chains/ethereum/contracts/forge-test/Entropy.t.sol

@@ -10,6 +10,7 @@ import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
 import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
 import "./utils/EntropyTestUtils.t.sol";
 import "../contracts/entropy/EntropyUpgradable.sol";
+import "@pythnetwork/entropy-sdk-solidity/EntropyStatusConstants.sol";
 
 // TODO
 // - fuzz test?
@@ -804,7 +805,7 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents {
                 blockNumber: 1234,
                 requester: user1,
                 useBlockhash: false,
-                isRequestWithCallback: true
+                callbackStatus: EntropyStatusConstants.CALLBACK_NOT_STARTED
             })
         );
         vm.roll(1234);
@@ -835,7 +836,7 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents {
     function testRequestWithCallbackAndRevealWithCallbackByContract() public {
         bytes32 userRandomNumber = bytes32(uint(42));
         uint fee = random.getFee(provider1);
-        EntropyConsumer consumer = new EntropyConsumer(address(random));
+        EntropyConsumer consumer = new EntropyConsumer(address(random), false);
         vm.deal(user1, fee);
         vm.prank(user1);
         uint64 assignedSequenceNumber = consumer.requestEntropy{value: fee}(
@@ -938,12 +939,13 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents {
         );
     }
 
+    // TODO: restore this test once providers have custom gas limits, which allow us to toggle between
+    // the old and new failure behavior.
+    /*
     function testRequestWithCallbackAndRevealWithCallbackFailing() public {
         bytes32 userRandomNumber = bytes32(uint(42));
         uint fee = random.getFee(provider1);
-        EntropyConsumerFails consumer = new EntropyConsumerFails(
-            address(random)
-        );
+        EntropyConsumer consumer = new EntropyConsumer(address(random), true);
         vm.deal(address(consumer), fee);
         vm.startPrank(address(consumer));
         uint64 assignedSequenceNumber = random.requestWithCallback{value: fee}(
@@ -959,6 +961,155 @@ contract EntropyTest is Test, EntropyTestUtils, EntropyEvents {
             provider1Proofs[assignedSequenceNumber]
         );
     }
+    */
+
+    function testRequestWithRevertingCallback() public {
+        bytes32 userRandomNumber = bytes32(uint(42));
+        uint fee = random.getFee(provider1);
+        EntropyConsumer consumer = new EntropyConsumer(address(random), true);
+        vm.deal(user1, fee);
+        vm.prank(user1);
+        uint64 assignedSequenceNumber = consumer.requestEntropy{value: fee}(
+            userRandomNumber
+        );
+
+        // On the first attempt, the transaction should succeed and emit CallbackFailed event.
+        bytes memory revertReason = abi.encodeWithSelector(
+            0x08c379a0,
+            "Callback failed"
+        );
+        vm.expectEmit(false, false, false, true, address(random));
+        emit CallbackFailed(
+            provider1,
+            address(consumer),
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber],
+            random.combineRandomValues(
+                userRandomNumber,
+                provider1Proofs[assignedSequenceNumber],
+                0
+            ),
+            revertReason
+        );
+        random.revealWithCallback(
+            provider1,
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber]
+        );
+
+        // Verify request is still active after failure
+        EntropyStructs.Request memory reqAfterFailure = random.getRequest(
+            provider1,
+            assignedSequenceNumber
+        );
+        assertEq(reqAfterFailure.sequenceNumber, assignedSequenceNumber);
+        assertTrue(
+            reqAfterFailure.callbackStatus ==
+                EntropyStatusConstants.CALLBACK_FAILED
+        );
+
+        // On the second attempt, the transaction should directly revert
+        vm.expectRevert();
+        random.revealWithCallback(
+            provider1,
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber]
+        );
+
+        // Again, request stays active after failure
+        reqAfterFailure = random.getRequest(provider1, assignedSequenceNumber);
+        assertEq(reqAfterFailure.sequenceNumber, assignedSequenceNumber);
+        assertTrue(
+            reqAfterFailure.callbackStatus ==
+                EntropyStatusConstants.CALLBACK_FAILED
+        );
+
+        // If the callback starts succeeding, we can invoke it and it emits the usual RevealedWithCallback event.
+        consumer.setReverts(false);
+        vm.expectEmit(false, false, false, true, address(random));
+        emit RevealedWithCallback(
+            reqAfterFailure,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber],
+            random.combineRandomValues(
+                userRandomNumber,
+                provider1Proofs[assignedSequenceNumber],
+                0
+            )
+        );
+        random.revealWithCallback(
+            provider1,
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber]
+        );
+
+        // Verify request is cleared after successful reveal
+        EntropyStructs.Request memory reqAfterReveal = random.getRequest(
+            provider1,
+            assignedSequenceNumber
+        );
+        assertEq(reqAfterReveal.sequenceNumber, 0);
+    }
+
+    function testRequestWithCallbackUsingTooMuchGas() public {
+        uint64 defaultGasLimit = 100000;
+
+        bytes32 userRandomNumber = bytes32(uint(42));
+        uint fee = random.getFee(provider1);
+        EntropyConsumer consumer = new EntropyConsumer(address(random), false);
+        // Consumer callback uses ~10% more gas than the provider's default
+        consumer.setTargetGasUsage((defaultGasLimit * 110) / 100);
+
+        vm.deal(user1, fee);
+        vm.prank(user1);
+        uint64 assignedSequenceNumber = consumer.requestEntropy{value: fee}(
+            userRandomNumber
+        );
+        EntropyStructs.Request memory req = random.getRequest(
+            provider1,
+            assignedSequenceNumber
+        );
+
+        // The transaction reverts if the provider does not provide enough gas to forward
+        // the gasLimit to the callback transaction.
+        vm.expectRevert();
+        random.revealWithCallback{gas: defaultGasLimit}(
+            provider1,
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber]
+        );
+
+        // Calling without a gas limit should succeed
+        vm.expectEmit(false, false, false, true, address(random));
+        emit RevealedWithCallback(
+            req,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber],
+            random.combineRandomValues(
+                userRandomNumber,
+                provider1Proofs[assignedSequenceNumber],
+                0
+            )
+        );
+        random.revealWithCallback(
+            provider1,
+            assignedSequenceNumber,
+            userRandomNumber,
+            provider1Proofs[assignedSequenceNumber]
+        );
+
+        // Verify request is cleared after successful reveal
+        EntropyStructs.Request memory reqAfterReveal = random.getRequest(
+            provider1,
+            assignedSequenceNumber
+        );
+        assertEq(reqAfterReveal.sequenceNumber, 0);
+    }
 
     function testLastRevealedTooOld() public {
         for (uint256 i = 0; i < provider1MaxNumHashes; i++) {
@@ -1155,9 +1306,14 @@ contract EntropyConsumer is IEntropyConsumer {
     bytes32 public randomness;
     address public entropy;
     address public provider;
+    bool public reverts;
+    uint256 public gasUsed;
+    uint256 public targetGasUsage;
 
-    constructor(address _entropy) {
+    constructor(address _entropy, bool _reverts) {
         entropy = _entropy;
+        reverts = _reverts;
+        targetGasUsage = 0; // Default target
     }
 
     function requestEntropy(
@@ -1173,31 +1329,37 @@ contract EntropyConsumer is IEntropyConsumer {
         return entropy;
     }
 
+    function setReverts(bool _reverts) public {
+        reverts = _reverts;
+    }
+
+    function setTargetGasUsage(uint256 _targetGasUsage) public {
+        targetGasUsage = _targetGasUsage;
+    }
+
     function entropyCallback(
         uint64 _sequence,
         address _provider,
         bytes32 _randomness
     ) internal override {
-        sequence = _sequence;
-        provider = _provider;
-        randomness = _randomness;
-    }
-}
-
-contract EntropyConsumerFails is IEntropyConsumer {
-    uint64 public sequence;
-    bytes32 public randomness;
-    address public entropy;
-
-    constructor(address _entropy) {
-        entropy = _entropy;
-    }
+        uint256 startGas = gasleft();
+        uint256 currentGasUsed = 0;
+
+        // Keep consuming gas until we reach our target
+        while (currentGasUsed < targetGasUsage) {
+            // Consume gas with a hash operation
+            keccak256(abi.encodePacked(currentGasUsed, _randomness));
+            currentGasUsed = startGas - gasleft();
+        }
 
-    function getEntropy() internal view override returns (address) {
-        return entropy;
-    }
+        gasUsed = currentGasUsed;
 
-    function entropyCallback(uint64, address, bytes32) internal pure override {
-        revert("Callback failed");
+        if (!reverts) {
+            sequence = _sequence;
+            provider = _provider;
+            randomness = _randomness;
+        } else {
+            revert("Callback failed");
+        }
     }
 }

+ 2 - 1
target_chains/ethereum/contracts/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-contract",
-  "version": "1.4.4-alpha.1",
+  "version": "1.4.4-alpha.2",
   "description": "",
   "private": "true",
   "devDependencies": {
@@ -39,6 +39,7 @@
     "@openzeppelin/contracts": "=4.8.1",
     "@openzeppelin/contracts-upgradeable": "=4.8.1",
     "@openzeppelin/hardhat-upgrades": "^1.22.1",
+    "@nomad-xyz/excessively-safe-call": "^0.0.1-rc.1",
     "@pythnetwork/contract-manager": "workspace:*",
     "@pythnetwork/entropy-sdk-solidity": "workspace:*",
     "@pythnetwork/pyth-sdk-solidity": "workspace:*",

+ 1 - 0
target_chains/ethereum/contracts/remappings.txt

@@ -3,4 +3,5 @@
 @pythnetwork/=./node_modules/@pythnetwork/
 ds-test/=lib/forge-std/lib/ds-test/src/
 forge-std/=lib/forge-std/src/
+@nomad-xyz=./node_modules/@nomad-xyz/
 truffle/=./node_modules/truffle/

+ 10 - 0
target_chains/ethereum/entropy_sdk/solidity/EntropyEvents.sol

@@ -29,6 +29,16 @@ interface EntropyEvents {
         bytes32 randomNumber
     );
 
+    event CallbackFailed(
+        address indexed provider,
+        address indexed requestor,
+        uint64 indexed sequenceNumber,
+        bytes32 userRandomNumber,
+        bytes32 providerRevelation,
+        bytes32 randomNumber,
+        bytes errorCode
+    );
+
     event ProviderFeeUpdated(address provider, uint128 oldFee, uint128 newFee);
 
     event ProviderUriUpdated(address provider, bytes oldUri, bytes newUri);

+ 13 - 0
target_chains/ethereum/entropy_sdk/solidity/EntropyStatusConstants.sol

@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: Apache 2
+
+library EntropyStatusConstants {
+    // Status values for Request.status //
+    // not a request with callback
+    uint8 public constant CALLBACK_NOT_NECESSARY = 0;
+    // A request with callback where the callback hasn't been invoked yet.
+    uint8 public constant CALLBACK_NOT_STARTED = 1;
+    // A request with callback where the callback is currently in flight (this state is a reentry guard).
+    uint8 public constant CALLBACK_IN_PROGRESS = 2;
+    // A request with callback where the callback has been invoked and failed.
+    uint8 public constant CALLBACK_FAILED = 3;
+}

+ 3 - 3
target_chains/ethereum/entropy_sdk/solidity/EntropyStructs.sol

@@ -59,8 +59,8 @@ contract EntropyStructs {
         address requester;
         // If true, incorporate the blockhash of blockNumber into the generated random value.
         bool useBlockhash;
-        // If true, the requester will be called back with the generated random value.
-        bool isRequestWithCallback;
-        // There are 2 remaining bytes of free space in this slot.
+        // Status flag for requests with callbacks. See EntropyConstants for the possible values of this flag.
+        uint8 callbackStatus;
+        // 2 bytes of space left in this struct.
     }
 }

+ 61 - 12
target_chains/ethereum/entropy_sdk/solidity/abis/EntropyEvents.json

@@ -1,4 +1,53 @@
 [
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "requestor",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint64",
+        "name": "sequenceNumber",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "userRandomNumber",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "providerRevelation",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "randomNumber",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes",
+        "name": "errorCode",
+        "type": "bytes"
+      }
+    ],
+    "name": "CallbackFailed",
+    "type": "event"
+  },
   {
     "anonymous": false,
     "inputs": [
@@ -215,9 +264,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -294,9 +343,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -349,9 +398,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -428,9 +477,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,

+ 64 - 15
target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json

@@ -1,4 +1,53 @@
 [
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "provider",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "requestor",
+        "type": "address"
+      },
+      {
+        "indexed": true,
+        "internalType": "uint64",
+        "name": "sequenceNumber",
+        "type": "uint64"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "userRandomNumber",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "providerRevelation",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes32",
+        "name": "randomNumber",
+        "type": "bytes32"
+      },
+      {
+        "indexed": false,
+        "internalType": "bytes",
+        "name": "errorCode",
+        "type": "bytes"
+      }
+    ],
+    "name": "CallbackFailed",
+    "type": "event"
+  },
   {
     "anonymous": false,
     "inputs": [
@@ -215,9 +264,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -294,9 +343,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -349,9 +398,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -428,9 +477,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "indexed": false,
@@ -735,9 +784,9 @@
             "type": "bool"
           },
           {
-            "internalType": "bool",
-            "name": "isRequestWithCallback",
-            "type": "bool"
+            "internalType": "uint8",
+            "name": "callbackStatus",
+            "type": "uint8"
           }
         ],
         "internalType": "struct EntropyStructs.Request",