diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 3b98f944582..d1bec52c391 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -20,6 +20,7 @@ block_number = 12345 solc_version = '0.8.19' src = 'src/v0.8/functions' test = 'src/v0.8/functions/tests' +gas_price = 3000000000 [profile.vrf] optimizer_runs = 1000 diff --git a/contracts/gas-snapshots/functions.gas-snapshot b/contracts/gas-snapshots/functions.gas-snapshot index 9b501e677b9..0664a2eb997 100644 --- a/contracts/gas-snapshots/functions.gas-snapshot +++ b/contracts/gas-snapshots/functions.gas-snapshot @@ -13,17 +13,17 @@ FunctionsOracle_setRegistry:testSetRegistrySuccess() (gas: 35791) FunctionsOracle_setRegistry:testSetRegistry_gas() (gas: 31987) FunctionsOracle_typeAndVersion:testTypeAndVersionSuccess() (gas: 6905) FunctionsRouter_Constructor:test_Constructor_Success() (gas: 12073) -FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedCostExceedsCommitment() (gas: 48079) -FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInsufficientGas() (gas: 38613) -FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInvalidCommitment() (gas: 36022) -FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInvalidRequestId() (gas: 35147) -FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedSubscriptionBalanceInvariant() (gas: 210) -FunctionsRouter_Fulfill:test_Fulfill_RevertIfNotCommittedCoordinator() (gas: 27993) -FunctionsRouter_Fulfill:test_Fulfill_RevertIfPaused() (gas: 33184) -FunctionsRouter_Fulfill:test_Fulfill_SuccessClientNoLongerExists() (gas: 93416) -FunctionsRouter_Fulfill:test_Fulfill_SuccessFulfilled() (gas: 103212) -FunctionsRouter_Fulfill:test_Fulfill_SuccessUserCallbackReverts() (gas: 1762498) -FunctionsRouter_Fulfill:test_Fulfill_SuccessUserCallbackRunsOutOfGas() (gas: 205574) +FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedCostExceedsCommitment() (gas: 172948) +FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInsufficientGas() (gas: 163234) +FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInvalidCommitment() (gas: 38092) +FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedInvalidRequestId() (gas: 35224) +FunctionsRouter_Fulfill:test_Fulfill_RequestNotProcessedSubscriptionBalanceInvariant() (gas: 181443) +FunctionsRouter_Fulfill:test_Fulfill_RevertIfNotCommittedCoordinator() (gas: 28063) +FunctionsRouter_Fulfill:test_Fulfill_RevertIfPaused() (gas: 156949) +FunctionsRouter_Fulfill:test_Fulfill_SuccessClientNoLongerExists() (gas: 297578) +FunctionsRouter_Fulfill:test_Fulfill_SuccessFulfilled() (gas: 311147) +FunctionsRouter_Fulfill:test_Fulfill_SuccessUserCallbackReverts() (gas: 2076842) +FunctionsRouter_Fulfill:test_Fulfill_SuccessUserCallbackRunsOutOfGas() (gas: 515646) FunctionsRouter_GetAdminFee:test_GetAdminFee_Success() (gas: 18005) FunctionsRouter_GetAllowListId:test_GetAllowListId_Success() (gas: 12926) FunctionsRouter_GetConfig:test_GetConfig_Success() (gas: 37136) @@ -38,13 +38,13 @@ FunctionsRouter_IsValidCallbackGasLimit:test_IsValidCallbackGasLimit_Success() ( FunctionsRouter_Pause:test_Pause_RevertIfNotOwner() (gas: 13315) FunctionsRouter_Pause:test_Pause_Success() (gas: 20298) FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfEmptyAddress() (gas: 14768) -FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfExceedsMaxProposal() (gas: 22661) +FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfExceedsMaxProposal() (gas: 21670) FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfLengthMismatch() (gas: 14647) FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfNotNewContract() (gas: 19025) -FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfNotOwner() (gas: 23326) -FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_Success() (gas: 118413) +FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_RevertIfNotOwner() (gas: 23369) +FunctionsRouter_ProposeContractsUpdate:test_ProposeContractsUpdate_Success() (gas: 118456) FunctionsRouter_SendRequest:test_SendRequest_RevertIfConsumerNotAllowed() (gas: 59304) -FunctionsRouter_SendRequest:test_SendRequest_RevertIfDuplicateRequestId() (gas: 192143) +FunctionsRouter_SendRequest:test_SendRequest_RevertIfDuplicateRequestId() (gas: 192134) FunctionsRouter_SendRequest:test_SendRequest_RevertIfEmptyData() (gas: 29405) FunctionsRouter_SendRequest:test_SendRequest_RevertIfIncorrectDonId() (gas: 57926) FunctionsRouter_SendRequest:test_SendRequest_RevertIfInsufficientSubscriptionBalance() (gas: 185987) @@ -70,21 +70,21 @@ FunctionsRouter_UpdateConfig:test_UpdateConfig_RevertIfNotOwner() (gas: 24437) FunctionsRouter_UpdateConfig:test_UpdateConfig_Success() (gas: 60653) FunctionsRouter_UpdateContracts:test_UpdateContracts_RevertIfNotOwner() (gas: 13293) FunctionsRouter_UpdateContracts:test_UpdateContracts_Success() (gas: 38716) -FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfNotAllowedSender() (gas: 60333) +FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfNotAllowedSender() (gas: 60324) FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfPaused() (gas: 60962) -FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfSenderBecomesBlocked() (gas: 94681) +FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfSenderBecomesBlocked() (gas: 94675) FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_RevertIfSenderIsNotNewOwner() (gas: 62691) FunctionsSubscriptions_AcceptSubscriptionOwnerTransfer:test_AcceptSubscriptionOwnerTransfer_Success() (gas: 59679) FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfMaximumConsumers() (gas: 137833) FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfMaximumConsumersAfterConfigUpdate() (gas: 164777) FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfNoSubscription() (gas: 12926) FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfNotAllowedSender() (gas: 57789) -FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfNotSubscriptionOwner() (gas: 87166) +FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfNotSubscriptionOwner() (gas: 87142) FunctionsSubscriptions_AddConsumer:test_AddConsumer_RevertIfPaused() (gas: 18051) FunctionsSubscriptions_AddConsumer:test_AddConsumer_Success() (gas: 95481) FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfNoSubscription() (gas: 15085) FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfNotAllowedSender() (gas: 57929) -FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfNotSubscriptionOwner() (gas: 89340) +FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfNotSubscriptionOwner() (gas: 89316) FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfPaused() (gas: 20191) FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_RevertIfPendingRequests() (gas: 193277) FunctionsSubscriptions_CancelSubscription:test_CancelSubscription_SuccessForfeitAllBalanceAsDeposit() (gas: 113352) @@ -139,38 +139,38 @@ FunctionsSubscriptions_RecoverFunds:test_RecoverFunds_Success() (gas: 41093) FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfInvalidConsumer() (gas: 30238) FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfNoSubscription() (gas: 14997) FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfNotAllowedSender() (gas: 57778) -FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfNotSubscriptionOwner() (gas: 87210) +FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfNotSubscriptionOwner() (gas: 87186) FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfPaused() (gas: 18004) -FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfPendingRequests() (gas: 190701) +FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_RevertIfPendingRequests() (gas: 190709) FunctionsSubscriptions_RemoveConsumer:test_RemoveConsumer_Success() (gas: 41979) FunctionsSubscriptions_SetFlags:test_SetFlags_RevertIfNoSubscription() (gas: 12847) FunctionsSubscriptions_SetFlags:test_SetFlags_RevertIfNotOwner() (gas: 15640) FunctionsSubscriptions_SetFlags:test_SetFlags_Success() (gas: 35549) -FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertIfPaused() (gas: 25859) -FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertIfTimeoutNotExceeded() (gas: 25188) -FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertInvalidRequest() (gas: 28142) -FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_Success() (gas: 57634) +FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertIfPaused() (gas: 25910) +FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertIfTimeoutNotExceeded() (gas: 25239) +FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_RevertInvalidRequest() (gas: 28220) +FunctionsSubscriptions_TimeoutRequests:test_TimeoutRequests_Success() (gas: 57730) FunctionsSubscriptions_createSubscription:test_CreateSubscription_RevertIfNotAllowedSender() (gas: 26368) FunctionsSubscriptions_createSubscription:test_CreateSubscription_RevertIfPaused() (gas: 15714) FunctionsSubscriptions_createSubscription:test_CreateSubscription_Success() (gas: 152510) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfAcceptorIsNotSender() (gas: 25855) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfBlockedSender() (gas: 44366) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfInvalidSigner() (gas: 23615) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfRecipientContractIsNotSender() (gas: 1458273) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfRecipientIsNotSender() (gas: 26021) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_SuccessIfAcceptingForContract() (gas: 1538382) -FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_SuccessIfAcceptingForSelf() (gas: 94702) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfAcceptorIsNotSender() (gas: 25837) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfBlockedSender() (gas: 44348) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfInvalidSigner() (gas: 23597) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfRecipientContractIsNotSender() (gas: 1458254) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_RevertIfRecipientIsNotSender() (gas: 26003) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_SuccessIfAcceptingForContract() (gas: 1538373) +FunctionsTermsOfServiceAllowList_AcceptTermsOfService:test_AcceptTermsOfService_SuccessIfAcceptingForSelf() (gas: 94684) FunctionsTermsOfServiceAllowList_BlockSender:test_BlockSender_RevertIfNotOwner() (gas: 15469) -FunctionsTermsOfServiceAllowList_BlockSender:test_BlockSender_Success() (gas: 50442) +FunctionsTermsOfServiceAllowList_BlockSender:test_BlockSender_Success() (gas: 50421) FunctionsTermsOfServiceAllowList_Constructor:test_Constructor_Success() (gas: 12187) FunctionsTermsOfServiceAllowList_GetAllAllowedSenders:test_GetAllAllowedSenders_Success() (gas: 19243) FunctionsTermsOfServiceAllowList_GetConfig:test_GetConfig_Success() (gas: 15773) -FunctionsTermsOfServiceAllowList_GetMessage:test_GetMessage_Success() (gas: 11592) -FunctionsTermsOfServiceAllowList_HasAccess:test_HasAccess_FalseWhenEnabled() (gas: 15931) -FunctionsTermsOfServiceAllowList_HasAccess:test_HasAccess_TrueWhenDisabled() (gas: 23445) +FunctionsTermsOfServiceAllowList_GetMessage:test_GetMessage_Success() (gas: 11583) +FunctionsTermsOfServiceAllowList_HasAccess:test_HasAccess_FalseWhenEnabled() (gas: 15925) +FunctionsTermsOfServiceAllowList_HasAccess:test_HasAccess_TrueWhenDisabled() (gas: 23430) FunctionsTermsOfServiceAllowList_IsBlockedSender:test_IsBlockedSender_SuccessFalse() (gas: 15354) FunctionsTermsOfServiceAllowList_IsBlockedSender:test_IsBlockedSender_SuccessTrue() (gas: 41957) FunctionsTermsOfServiceAllowList_UnblockSender:test_UnblockSender_RevertIfNotOwner() (gas: 13525) -FunctionsTermsOfServiceAllowList_UnblockSender:test_UnblockSender_Success() (gas: 95208) -FunctionsTermsOfServiceAllowList_UpdateConfig:test_UpdateConfig_RevertIfNotOwner() (gas: 13736) -FunctionsTermsOfServiceAllowList_UpdateConfig:test_UpdateConfig_Success() (gas: 22082) \ No newline at end of file +FunctionsTermsOfServiceAllowList_UnblockSender:test_UnblockSender_Success() (gas: 95184) +FunctionsTermsOfServiceAllowList_UpdateConfig:test_UpdateConfig_RevertIfNotOwner() (gas: 13727) +FunctionsTermsOfServiceAllowList_UpdateConfig:test_UpdateConfig_Success() (gas: 22073) \ No newline at end of file diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol b/contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol index d01e438a92a..b012732897f 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_0_0/BaseTest.t.sol @@ -11,6 +11,10 @@ contract BaseTest is Test { uint256 internal STRANGER_PRIVATE_KEY = 0x2; address internal STRANGER_ADDRESS = vm.addr(STRANGER_PRIVATE_KEY); + uint256 TX_GASPRICE_START = 3000000000; // 3 gwei + + uint72 constant JUELS_PER_LINK = 1e18; + function setUp() public virtual { // BaseTest.setUp is often called multiple times from tests' setUp due to inheritance. if (s_baseTestInitialized) return; diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol index 2773baa47a4..088de631d06 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsCoordinator.t.sol @@ -173,7 +173,7 @@ contract FunctionsBilling_EstimateCost is FunctionsSubscriptionSetup { callbackGasLimit, gasPriceWei ); - uint96 expectedCostEstimate = 15725380; + uint96 expectedCostEstimate = 10873200; assertEq(costEstimate, expectedCostEstimate); } } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol index e4bf442ec1b..1a858c28df6 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsRouter.t.sol @@ -817,19 +817,21 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { function test_Fulfill_RevertIfPaused() public { s_functionsRouter.pause(); - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + uint256 requestToFulfill = 1; - bytes memory response = bytes("hello world!"); + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + errors[0] = err; vm.expectRevert("Pausable: paused"); - s_functionsRouter.fulfill(response, err, juelsPerGas, costWithoutCallback, transmitter, commitment); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, false); } function test_Fulfill_RevertIfNotCommittedCoordinator() public { @@ -842,7 +844,7 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { uint96 juelsPerGas = 0; uint96 costWithoutCallback = 0; address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + FunctionsResponse.Commitment memory commitment = s_requests[1].commitment; vm.expectRevert(FunctionsRouter.OnlyCallableFromCoordinator.selector); s_functionsRouter.fulfill(response, err, juelsPerGas, costWithoutCallback, transmitter, commitment); @@ -865,7 +867,7 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { uint96 juelsPerGas = 0; uint96 costWithoutCallback = 0; address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + FunctionsResponse.Commitment memory commitment = s_requests[1].commitment; // Modify request commitment to have a invalid requestId bytes32 invalidRequestId = bytes32("this does not exist"); commitment.requestId = invalidRequestId; @@ -877,7 +879,7 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { bool checkData = true; vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); emit RequestNotProcessed({ - requestId: s_requestCommitment.requestId, + requestId: s_requests[1].requestId, coordinator: address(s_functionsCoordinator), transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.INVALID_REQUEST_ID @@ -906,7 +908,7 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { uint96 juelsPerGas = 0; uint96 costWithoutCallback = 0; address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + FunctionsResponse.Commitment memory commitment = s_requests[1].commitment; // Modify request commitment to have charge more than quoted commitment.estimatedTotalCostJuels = 10 * JUELS_PER_LINK; // 10 LINK @@ -917,7 +919,7 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { bool checkData = true; vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); emit RequestNotProcessed({ - requestId: s_requestCommitment.requestId, + requestId: s_requests[1].requestId, coordinator: address(s_functionsCoordinator), transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.INVALID_COMMITMENT @@ -937,114 +939,115 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { } function test_Fulfill_RequestNotProcessedInsufficientGas() public { - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + uint256 requestToFulfill = 1; - bytes memory response = bytes("hello world!"); + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; + errors[0] = err; - vm.txGasPrice(1); + uint32 callbackGasLimit = s_requests[requestToFulfill].requestData.callbackGasLimit; + // Coordinator sends enough gas that would get through callback and payment, but fail after + uint256 gasToUse = getCoordinatorConfig().gasOverheadBeforeCallback + callbackGasLimit + 100000; - // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - bool checkTopic1 = false; + // topic0 (function signature, always checked), topic1 (true), NOT topic2 (false), NOT topic3 (false), and data (true). + bool checkTopic1RequestId = true; bool checkTopic2 = false; bool checkTopic3 = false; bool checkData = true; - vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); + vm.expectEmit(checkTopic1RequestId, checkTopic2, checkTopic3, checkData); emit RequestNotProcessed({ - requestId: s_requestCommitment.requestId, + requestId: s_requests[requestToFulfill].requestId, coordinator: address(s_functionsCoordinator), transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.INSUFFICIENT_GAS_PROVIDED }); - // Coordinator sends enough gas that would get through callback and payment, but fail after - uint32 callbackGasLimit = 5000; - uint256 gasToUse = getCoordinatorConfig().gasOverheadBeforeCallback + callbackGasLimit; - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill{ - gas: gasToUse - }(response, err, juelsPerGas, costWithoutCallback, transmitter, s_requestCommitment); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.INSUFFICIENT_GAS_PROVIDED)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, false, 1, gasToUse); } function test_Fulfill_RequestNotProcessedSubscriptionBalanceInvariant() public { - // // Send as committed Coordinator - // vm.stopPrank(); - // vm.startPrank(address(s_functionsCoordinator)); - // bytes memory response = bytes("hello world!"); - // bytes memory err = new bytes(0); - // uint96 juelsPerGas = 0; - // uint96 costWithoutCallback = 0; - // address transmitter = NOP_TRANSMITTER_ADDRESS_1; - // FunctionsResponse.Commitment memory commitment = s_requestCommitment; - // TODO: use contract helper to change subscription balance - // // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - // bool checkTopic1 = false; - // bool checkTopic2 = false; - // bool checkTopic3 = false; - // bool checkData = true; - // vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); - // emit RequestNotProcessed({ - // requestId: s_requestCommitment.requestId, - // coordinator: address(s_functionsCoordinator), - // transmitter: NOP_TRANSMITTER_ADDRESS_1, - // resultCode: FunctionsResponse.FulfillResult.SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION - // }); - // (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - // response, - // err, - // juelsPerGas, - // costWithoutCallback, - // transmitter, - // commitment - // ); - // assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION)); - // assertEq(callbackGasCostJuels, 0); + // Find the storage slot that the Subscription is on + vm.record(); + s_functionsRouter.getSubscription(s_subscriptionId); + (bytes32[] memory reads, ) = vm.accesses(address(s_functionsRouter)); + // The first read is from '_isExistingSubscription' which checks Subscription.owner on slot 0 + // Slot 0 is shared with the Subscription.balance + uint256 slot = uint256(reads[0]); + + // The request has already been initiated, forcibly lower the subscription's balance by clearing out slot 0 + uint96 balance = 1; + address owner = address(0); + bytes32 data = bytes32(abi.encodePacked(balance, owner)); // TODO: make this more accurate + vm.store(address(s_functionsRouter), bytes32(uint256(slot)), data); + + uint256 requestToFulfill = 1; + + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); + bytes memory err = new bytes(0); + errors[0] = err; + + // topic0 (function signature, always checked), topic1 (true), NOT topic2 (false), NOT topic3 (false), and data (true). + bool checkTopic1RequestId = true; + bool checkTopic2 = false; + bool checkTopic3 = false; + bool checkData = true; + vm.expectEmit(checkTopic1RequestId, checkTopic2, checkTopic3, checkData); + emit RequestNotProcessed({ + requestId: s_requests[requestToFulfill].requestId, + coordinator: address(s_functionsCoordinator), + transmitter: NOP_TRANSMITTER_ADDRESS_1, + resultCode: FunctionsResponse.FulfillResult.SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION + }); + + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, false); } function test_Fulfill_RequestNotProcessedCostExceedsCommitment() public { - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + // Use higher juelsPerGas than request time + // 10x the gas price + vm.txGasPrice(TX_GASPRICE_START * 10); - bytes memory response = bytes("hello world!"); + uint256 requestToFulfill = 1; + + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - // Use higher juelsPerGas than request time - uint96 juelsPerGas = 100000; - uint96 costWithoutCallback = 1; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + errors[0] = err; - // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - bool checkTopic1 = false; + // topic0 (function signature, always checked), topic1 (true), NOT topic2 (false), NOT topic3 (false), and data (true). + bool checkTopic1RequestId = true; bool checkTopic2 = false; bool checkTopic3 = false; bool checkData = true; - vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); + vm.expectEmit(checkTopic1RequestId, checkTopic2, checkTopic3, checkData); emit RequestNotProcessed({ - requestId: s_requestCommitment.requestId, + requestId: s_requests[requestToFulfill].requestId, coordinator: address(s_functionsCoordinator), transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.COST_EXCEEDS_COMMITMENT }); - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - response, - err, - juelsPerGas, - costWithoutCallback, - transmitter, - commitment - ); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.COST_EXCEEDS_COMMITMENT)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, false); } event RequestProcessed( @@ -1069,14 +1072,17 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { s_functionsRouter.addConsumer(s_subscriptionId, address(s_clientWithFailingCallback)); // Send a minimal request + uint256 requestKey = 99; + string memory sourceCode = "return 'hello world';"; + uint32 callbackGasLimit = 5500; vm.recordLogs(); bytes32 requestId = s_clientWithFailingCallback.sendSimpleRequestWithJavaScript( sourceCode, s_subscriptionId, s_donId, - 5000 + callbackGasLimit ); // Get commitment data from OracleRequest event log @@ -1085,129 +1091,109 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { entries[0].data, (address, uint64, address, bytes, uint16, bytes32, uint64, FunctionsResponse.Commitment) ); - s_requestCommitment = _commitment; - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + s_requests[requestKey] = Request({ + requestData: RequestData({ + sourceCode: sourceCode, + secrets: new bytes(0), + args: new string[](0), + bytesArgs: new bytes[](0), + callbackGasLimit: callbackGasLimit + }), + requestId: requestId, + commitment: _commitment + }); - bytes memory response = bytes("hello world!"); + // Fulfill + uint256 requestToFulfill = requestKey; + + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + errors[0] = err; - // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - bool checkTopic1 = false; + // topic0 (function signature, always checked), topic1 (true), NOT topic2 (false), NOT topic3 (false), and data (true). + bool checkTopic1RequestId = false; bool checkTopic2 = false; bool checkTopic3 = false; bool checkData = true; - vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); + vm.expectEmit(checkTopic1RequestId, checkTopic2, checkTopic3, checkData); emit RequestProcessed({ requestId: requestId, subscriptionId: s_subscriptionId, - totalCostJuels: s_adminFee + costWithoutCallback, - transmitter: transmitter, + totalCostJuels: _getExpectedCost(1379), // gasUsed is manually taken + transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR, - response: response, + response: bytes(response), err: err, callbackReturnData: vm.parseBytes( "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f61736b656420746f207265766572740000000000000000000000000000000000" - ) // TODO: build this + ) // TODO: build this programatically }); - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - response, - err, - juelsPerGas, - costWithoutCallback, - transmitter, - commitment - ); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, true, 1); } function test_Fulfill_SuccessUserCallbackRunsOutOfGas() public { + // Send request #2 with no callback gas string memory sourceCode = "return 'hello world';"; - bytes memory secrets; + bytes memory secrets = new bytes(0); string[] memory args = new string[](0); bytes[] memory bytesArgs = new bytes[](0); uint32 callbackGasLimit = 0; + _sendAndStoreRequest(2, sourceCode, secrets, args, bytesArgs, callbackGasLimit); - vm.recordLogs(); - // Send a request with no gas for the callback - bytes32 requestId = s_functionsClient.sendRequest( - s_donId, - sourceCode, - secrets, - args, - bytesArgs, - s_subscriptionId, - callbackGasLimit - ); + uint256 requestToFulfill = 2; - // Get commitment data from OracleRequest event log - Vm.Log[] memory entries = vm.getRecordedLogs(); - (, , , , , , , FunctionsResponse.Commitment memory commitment) = abi.decode( - entries[0].data, - (address, uint64, address, bytes, uint16, bytes32, uint64, FunctionsResponse.Commitment) - ); + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; - bytes memory response = bytes("hello world!"); + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; + errors[0] = err; - // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - vm.expectEmit(false, false, false, true); + // topic0 (function signature, always checked), topic1: request ID(true), NOT topic2 (false), NOT topic3 (false), and data (true). + vm.expectEmit(true, false, false, true); emit RequestProcessed({ - requestId: requestId, + requestId: s_requests[requestToFulfill].requestId, subscriptionId: s_subscriptionId, - totalCostJuels: s_adminFee + costWithoutCallback, // NOTE: tx.gasprice is at 0, so no callback gas used - transmitter: transmitter, + totalCostJuels: _getExpectedCost(137), // gasUsed is manually taken + transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR, - response: response, + response: bytes(response), err: err, callbackReturnData: new bytes(0) }); - vm.recordLogs(); - - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - response, - err, - juelsPerGas, - costWithoutCallback, - transmitter, - commitment - ); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, true, 1); } function test_Fulfill_SuccessClientNoLongerExists() public { // Delete the Client contract in the time between request and fulfillment vm.etch(address(s_functionsClient), new bytes(0)); - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); + uint256 requestToFulfill = 1; - bytes memory response = bytes("hello world!"); + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; - FunctionsResponse.Commitment memory commitment = s_requestCommitment; + errors[0] = err; // topic0 (function signature, always checked), topic1 (true), topic2 (true), NOT topic3 (false), and data (true). bool checkTopic1RequestId = true; @@ -1216,72 +1202,49 @@ contract FunctionsRouter_Fulfill is FunctionsClientRequestSetup { bool checkData = true; vm.expectEmit(checkTopic1RequestId, checkTopic2SubscriptionId, checkTopic3, checkData); emit RequestProcessed({ - requestId: s_requestId, + requestId: s_requests[requestToFulfill].requestId, subscriptionId: s_subscriptionId, - totalCostJuels: s_adminFee + costWithoutCallback, // NOTE: tx.gasprice is at 0, so no callback gas used - transmitter: transmitter, + totalCostJuels: _getExpectedCost(0), // gasUsed is manually taken + transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR, - response: response, + response: bytes(response), err: err, callbackReturnData: new bytes(0) }); - vm.recordLogs(); - - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - response, - err, - juelsPerGas, - costWithoutCallback, - transmitter, - commitment - ); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.USER_CALLBACK_ERROR)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, true, 1); } function test_Fulfill_SuccessFulfilled() public { - // Send as committed Coordinator - vm.stopPrank(); - vm.startPrank(address(s_functionsCoordinator)); - - bytes memory response = bytes("hello world!"); + // Fulfill request 1 + uint256 requestToFulfill = 1; + + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = requestToFulfill; + string[] memory results = new string[](1); + string memory response = "hello world!"; + results[0] = response; + bytes[] memory errors = new bytes[](1); bytes memory err = new bytes(0); - uint96 juelsPerGas = 0; - uint96 costWithoutCallback = 0; - address transmitter = NOP_TRANSMITTER_ADDRESS_1; + errors[0] = err; - // topic0 (function signature, always checked), NOT topic1 (false), NOT topic2 (false), NOT topic3 (false), and data (true). - bool checkTopic1 = false; + // topic0 (function signature, always checked), topic1 (true), NOT topic2 (false), NOT topic3 (false), and data (true). + bool checkTopic1RequestId = true; bool checkTopic2 = false; bool checkTopic3 = false; bool checkData = true; - vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); + vm.expectEmit(checkTopic1RequestId, checkTopic2, checkTopic3, checkData); emit RequestProcessed({ - requestId: s_requestId, + requestId: s_requests[requestToFulfill].requestId, subscriptionId: s_subscriptionId, - totalCostJuels: s_adminFee + costWithoutCallback, // NOTE: tx.gasprice is at 0, so no callback gas used - transmitter: transmitter, + totalCostJuels: _getExpectedCost(5371), // gasUsed is manually taken + transmitter: NOP_TRANSMITTER_ADDRESS_1, resultCode: FunctionsResponse.FulfillResult.FULFILLED, - response: response, + response: bytes(response), err: err, callbackReturnData: new bytes(0) }); - - vm.recordLogs(); - - (FunctionsResponse.FulfillResult resultCode, uint96 callbackGasCostJuels) = s_functionsRouter.fulfill( - response, - err, - juelsPerGas, - costWithoutCallback, - transmitter, - s_requestCommitment - ); - - assertEq(uint(resultCode), uint(FunctionsResponse.FulfillResult.FULFILLED)); - assertEq(callbackGasCostJuels, 0); + _reportAndStore(requestNumberKeys, results, errors); } } @@ -1444,11 +1407,11 @@ contract FunctionsRouter_ProposeContractsUpdate is FunctionsRoutesSetup { // Generate some mock data bytes32[] memory proposedContractSetIds = new bytes32[](INVALID_PROPOSAL_SET_LENGTH); - for (uint8 i = 0; i < INVALID_PROPOSAL_SET_LENGTH; ++i) { + for (uint256 i = 0; i < INVALID_PROPOSAL_SET_LENGTH; ++i) { proposedContractSetIds[i] = bytes32(uint256(i + 111)); } address[] memory proposedContractSetAddresses = new address[](INVALID_PROPOSAL_SET_LENGTH); - for (uint8 i = 0; i < INVALID_PROPOSAL_SET_LENGTH; ++i) { + for (uint256 i = 0; i < INVALID_PROPOSAL_SET_LENGTH; ++i) { proposedContractSetAddresses[i] = address(uint160(uint(keccak256(abi.encodePacked(i + 111))))); } diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol index 26dd7af20f4..ea5ec0dd683 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_0_0/FunctionsSubscriptions.t.sol @@ -1205,15 +1205,15 @@ contract FunctionsSubscriptions_TimeoutRequests is FunctionsClientRequestSetup { vm.expectRevert("Pausable: paused"); FunctionsResponse.Commitment[] memory commitments = new FunctionsResponse.Commitment[](1); - commitments[0] = s_requestCommitment; + commitments[0] = s_requests[1].commitment; s_functionsRouter.timeoutRequests(commitments); } function test_TimeoutRequests_RevertInvalidRequest() public { // Modify the commitment so that it doesn't match - s_requestCommitment.donFee = 123456789; + s_requests[1].commitment.donFee = 123456789; FunctionsResponse.Commitment[] memory commitments = new FunctionsResponse.Commitment[](1); - commitments[0] = s_requestCommitment; + commitments[0] = s_requests[1].commitment; vm.expectRevert(FunctionsSubscriptions.InvalidCalldata.selector); s_functionsRouter.timeoutRequests(commitments); } @@ -1221,7 +1221,7 @@ contract FunctionsSubscriptions_TimeoutRequests is FunctionsClientRequestSetup { function test_TimeoutRequests_RevertIfTimeoutNotExceeded() public { vm.expectRevert(FunctionsSubscriptions.TimeoutNotExceeded.selector); FunctionsResponse.Commitment[] memory commitments = new FunctionsResponse.Commitment[](1); - commitments[0] = s_requestCommitment; + commitments[0] = s_requests[1].commitment; s_functionsRouter.timeoutRequests(commitments); } @@ -1238,13 +1238,13 @@ contract FunctionsSubscriptions_TimeoutRequests is FunctionsClientRequestSetup { bool checkTopic3 = false; bool checkData = true; vm.expectEmit(checkTopic1, checkTopic2, checkTopic3, checkData); - emit RequestTimedOut(s_requestId); + emit RequestTimedOut(s_requests[1].requestId); // Jump ahead in time past timeout timestamp - vm.warp(s_requestCommitment.timeoutTimestamp + 1); + vm.warp(s_requests[1].commitment.timeoutTimestamp + 1); FunctionsResponse.Commitment[] memory commitments = new FunctionsResponse.Commitment[](1); - commitments[0] = s_requestCommitment; + commitments[0] = s_requests[1].commitment; s_functionsRouter.timeoutRequests(commitments); // Releases blocked balance and increments completed requests diff --git a/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol b/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol index 1578834499b..73644e38636 100644 --- a/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol +++ b/contracts/src/v0.8/functions/tests/v1_0_0/Setup.t.sol @@ -13,6 +13,7 @@ import {MockLinkToken} from "../../../mocks/MockLinkToken.sol"; import "forge-std/Vm.sol"; +/// @notice Set up to deploy the following contracts: FunctionsRouter, FunctionsCoordinator, LINK/ETH Feed, ToS Allow List, and LINK token contract FunctionsRouterSetup is BaseTest { FunctionsRouter internal s_functionsRouter; FunctionsCoordinatorTestHelper internal s_functionsCoordinator; // TODO: use actual FunctionsCoordinator instead of helper @@ -25,7 +26,7 @@ contract FunctionsRouterSetup is BaseTest { uint72 internal s_donFee = 100; bytes4 internal s_handleOracleFulfillmentSelector = 0x0ca76175; uint16 s_subscriptionDepositMinimumRequests = 1; - uint72 s_subscriptionDepositJuels = 11 * 1e18; + uint72 s_subscriptionDepositJuels = 11 * JUELS_PER_LINK; int256 internal LINK_ETH_RATE = 6000000000000000; @@ -67,8 +68,8 @@ contract FunctionsRouterSetup is BaseTest { return FunctionsBilling.Config({ feedStalenessSeconds: 24 * 60 * 60, // 1 day - gasOverheadAfterCallback: 44_615, // TODO: update - gasOverheadBeforeCallback: 44_615, // TODO: update + gasOverheadAfterCallback: 50_000, // TODO: update + gasOverheadBeforeCallback: 100_00, // TODO: update requestTimeoutSeconds: 60 * 5, // 5 minutes donFee: s_donFee, maxSupportedRequestDataVersion: 1, @@ -82,6 +83,7 @@ contract FunctionsRouterSetup is BaseTest { } } +/// @notice Set up to set the OCR configuration of the Coordinator contract contract FunctionsDONSetup is FunctionsRouterSetup { uint256 internal NOP_SIGNER_PRIVATE_KEY_1 = 0x100; address internal NOP_SIGNER_ADDRESS_1 = vm.addr(NOP_SIGNER_PRIVATE_KEY_1); @@ -101,35 +103,41 @@ contract FunctionsDONSetup is FunctionsRouterSetup { uint256 internal NOP_TRANSMITTER_PRIVATE_KEY_4 = 0x107; address internal NOP_TRANSMITTER_ADDRESS_4 = vm.addr(NOP_TRANSMITTER_PRIVATE_KEY_4); + address[] internal s_signers; + address[] internal s_transmitters; + uint8 s_f = 1; + bytes internal s_onchainConfig = new bytes(0); + uint64 internal s_offchainConfigVersion = 1; + bytes internal s_offchainConfig = new bytes(0); + function setUp() public virtual override { FunctionsRouterSetup.setUp(); - address[] memory _signers = new address[](4); - _signers[0] = NOP_SIGNER_ADDRESS_1; - _signers[1] = NOP_SIGNER_ADDRESS_2; - _signers[2] = NOP_SIGNER_ADDRESS_3; - _signers[3] = NOP_SIGNER_ADDRESS_4; - address[] memory _transmitters = new address[](4); - _transmitters[0] = NOP_TRANSMITTER_ADDRESS_1; - _transmitters[1] = NOP_TRANSMITTER_ADDRESS_2; - _transmitters[2] = NOP_TRANSMITTER_ADDRESS_3; - _transmitters[3] = NOP_TRANSMITTER_ADDRESS_4; - uint8 _f = 1; - bytes memory _onchainConfig = new bytes(0); - uint64 _offchainConfigVersion = 1; - bytes memory _offchainConfig = new bytes(0); + s_signers = new address[](4); + s_signers[0] = NOP_SIGNER_ADDRESS_1; + s_signers[1] = NOP_SIGNER_ADDRESS_2; + s_signers[2] = NOP_SIGNER_ADDRESS_3; + s_signers[3] = NOP_SIGNER_ADDRESS_4; + + s_transmitters = new address[](4); + s_transmitters[0] = NOP_TRANSMITTER_ADDRESS_1; + s_transmitters[1] = NOP_TRANSMITTER_ADDRESS_2; + s_transmitters[2] = NOP_TRANSMITTER_ADDRESS_3; + s_transmitters[3] = NOP_TRANSMITTER_ADDRESS_4; + // set OCR config s_functionsCoordinator.setConfig( - _signers, - _transmitters, - _f, - _onchainConfig, - _offchainConfigVersion, - _offchainConfig + s_signers, + s_transmitters, + s_f, + s_onchainConfig, + s_offchainConfigVersion, + s_offchainConfig ); } } +/// @notice Set up to add the Coordinator and ToS Allow Contract as routes on the Router contract contract FunctionsRoutesSetup is FunctionsDONSetup { bytes32 s_donId = bytes32("1"); @@ -149,6 +157,7 @@ contract FunctionsRoutesSetup is FunctionsDONSetup { } } +/// @notice Set up for the OWNER_ADDRESS to accept the Terms of Service contract FunctionsOwnerAcceptTermsOfServiceSetup is FunctionsRoutesSetup { function setUp() public virtual override { FunctionsRoutesSetup.setUp(); @@ -160,6 +169,7 @@ contract FunctionsOwnerAcceptTermsOfServiceSetup is FunctionsRoutesSetup { } } +/// @notice Set up to deploy a consumer contract contract FunctionsClientSetup is FunctionsOwnerAcceptTermsOfServiceSetup { FunctionsClientUpgradeHelper internal s_functionsClient; @@ -170,8 +180,8 @@ contract FunctionsClientSetup is FunctionsOwnerAcceptTermsOfServiceSetup { } } +/// @notice Set up to create a subscription, add the consumer contract as a consumer of the subscription, and fund the subscription with 's_subscriptionInitialFunding' contract FunctionsSubscriptionSetup is FunctionsClientSetup { - uint96 constant JUELS_PER_LINK = 1e18; uint64 s_subscriptionId; uint96 s_subscriptionInitialFunding = 10 * JUELS_PER_LINK; // 10 LINK @@ -187,22 +197,68 @@ contract FunctionsSubscriptionSetup is FunctionsClientSetup { } } +/// @notice Set up to initate a minimal request and store it in s_requests[1] contract FunctionsClientRequestSetup is FunctionsSubscriptionSetup { - bytes32 s_requestId; - FunctionsResponse.Commitment s_requestCommitment; + struct RequestData { + string sourceCode; + bytes secrets; + string[] args; + bytes[] bytesArgs; + uint32 callbackGasLimit; + } + struct Request { + RequestData requestData; + bytes32 requestId; + FunctionsResponse.Commitment commitment; + } + + mapping(uint256 => Request) s_requests; + + uint96 s_fulfillmentRouterOwnerBalance = 0; + uint96 s_fulfillmentCoordinatorBalance = 0; function setUp() public virtual override { FunctionsSubscriptionSetup.setUp(); - // Send a minimal request + // Send request #1 string memory sourceCode = "return 'hello world';"; - bytes memory secrets; + bytes memory secrets = new bytes(0); string[] memory args = new string[](0); bytes[] memory bytesArgs = new bytes[](0); uint32 callbackGasLimit = 5500; + _sendAndStoreRequest(1, sourceCode, secrets, args, bytesArgs, callbackGasLimit); + } + + function _getExpectedCost(uint256 gasUsed) internal view returns (uint96 totalCostJuels) { + uint96 juelsPerGas = uint96((1e18 * TX_GASPRICE_START) / uint256(LINK_ETH_RATE)); + uint96 gasOverheadJuels = juelsPerGas * + (getCoordinatorConfig().gasOverheadBeforeCallback + getCoordinatorConfig().gasOverheadAfterCallback); + uint96 callbackGasCostJuels = uint96(juelsPerGas * gasUsed); + return gasOverheadJuels + s_donFee + s_adminFee + callbackGasCostJuels; + } + + /// @notice Send a request and store information about it in s_requests + /// @param requestNumberKey - the key that the request will be stored in `s_requests` in + /// @param sourceCode - Raw source code for Request.codeLocation of Location.Inline, URL for Request.codeLocation of Location.Remote, or slot decimal number for Request.codeLocation of Location.DONHosted + /// @param secrets - Encrypted URLs for Request.secretsLocation of Location.Remote (use addSecretsReference()), or CBOR encoded slotid+version for Request.secretsLocation of Location.DONHosted (use addDONHostedSecrets()) + /// @param args - String arguments that will be passed into the source code + /// @param bytesArgs - Bytes arguments that will be passed into the source code + /// @param callbackGasLimit - Gas limit for the fulfillment callback + function _sendAndStoreRequest( + uint256 requestNumberKey, + string memory sourceCode, + bytes memory secrets, + string[] memory args, + bytes[] memory bytesArgs, + uint32 callbackGasLimit + ) internal { + if (s_requests[requestNumberKey].requestId != bytes32(0)) { + revert("Request already written"); + } vm.recordLogs(); - s_requestId = s_functionsClient.sendRequest( + + bytes32 requestId = s_functionsClient.sendRequest( s_donId, sourceCode, secrets, @@ -218,184 +274,288 @@ contract FunctionsClientRequestSetup is FunctionsSubscriptionSetup { entries[0].data, (address, uint64, address, bytes, uint16, bytes32, uint64, FunctionsResponse.Commitment) ); - s_requestCommitment = commitment; + s_requests[requestNumberKey] = Request({ + requestData: RequestData({ + sourceCode: sourceCode, + secrets: secrets, + args: args, + bytesArgs: bytesArgs, + callbackGasLimit: callbackGasLimit + }), + requestId: requestId, + commitment: commitment + }); } -} - -contract FunctionsFulfillmentSetup is FunctionsClientRequestSetup { - uint96 s_fulfillmentRouterOwnerBalance = s_adminFee; - uint96 s_fulfillmentCoordinatorBalance; - - function setUp() public virtual override { - FunctionsClientRequestSetup.setUp(); - - // Send as transmitter 1 - vm.stopPrank(); - vm.startPrank(NOP_TRANSMITTER_ADDRESS_1); + /// @notice Send a request and store information about it in s_requests + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @return report - Report bytes data + /// @return reportContext - Report context bytes32 data + function _buildReport( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors + ) internal view returns (bytes memory report, bytes32[3] memory reportContext) { // Build report - bytes32[] memory requestIds = new bytes32[](1); - requestIds[0] = s_requestId; - bytes[] memory results = new bytes[](1); - results[0] = bytes("hello world!"); - bytes[] memory errors = new bytes[](1); - // No error - bytes[] memory onchainMetadata = new bytes[](1); - onchainMetadata[0] = abi.encode(s_requestCommitment); - bytes[] memory offchainMetadata = new bytes[](1); - // No offchain metadata - bytes memory report = abi.encode(requestIds, results, errors, onchainMetadata, offchainMetadata); - - // Build signers - address[31] memory signers; - signers[0] = NOP_SIGNER_ADDRESS_1; - - // Send report - vm.recordLogs(); - s_functionsCoordinator.callReportWithSigners(report, signers); - - // Get actual cost from RequestProcessed event log - Vm.Log[] memory entries = vm.getRecordedLogs(); - (uint96 totalCostJuels, , , , , ) = abi.decode( - entries[2].data, - (uint96, address, FunctionsResponse.FulfillResult, bytes, bytes, bytes) + bytes32[] memory _requestIds = new bytes32[](requestNumberKeys.length); + bytes[] memory _results = new bytes[](requestNumberKeys.length); + bytes[] memory _errors = new bytes[](requestNumberKeys.length); + bytes[] memory _onchainMetadata = new bytes[](requestNumberKeys.length); + bytes[] memory _offchainMetadata = new bytes[](requestNumberKeys.length); + for (uint256 i = 0; i < requestNumberKeys.length; ++i) { + if (keccak256(bytes(results[i])) != keccak256(new bytes(0)) && keccak256(errors[i]) != keccak256(new bytes(0))) { + revert("Report can only contain a result OR an error, one must remain empty."); + } + _requestIds[i] = s_requests[requestNumberKeys[i]].requestId; + _results[i] = bytes(results[i]); + _errors[i] = errors[i]; + _onchainMetadata[i] = abi.encode(s_requests[requestNumberKeys[i]].commitment); + _offchainMetadata[i] = new bytes(0); // No off-chain metadata + } + report = abi.encode(_requestIds, _results, _errors, _onchainMetadata, _offchainMetadata); + + // Build report context + uint256 h = uint256( + keccak256( + abi.encode( + block.chainid, + address(s_functionsCoordinator), + 1, + s_signers, + s_transmitters, + s_f, + s_onchainConfig, + s_offchainConfigVersion, + s_offchainConfig + ) + ) ); - // totalCostJuels = costWithoutCallbackJuels + adminFee + callbackGasCostJuels - s_fulfillmentCoordinatorBalance = totalCostJuels - s_adminFee; + uint256 prefixMask = type(uint256).max << (256 - 16); // 0xFFFF00..00 + uint256 prefix = 0x0001 << (256 - 16); // 0x000100..00 + bytes32 configDigest = bytes32((prefix & prefixMask) | (h & ~prefixMask)); + reportContext = [configDigest, configDigest, configDigest]; - // Return prank to Owner - vm.stopPrank(); - vm.startPrank(OWNER_ADDRESS); + return (report, reportContext); } -} - -contract FunctionsMultipleFulfillmentsSetup is FunctionsFulfillmentSetup { - bytes32 s_requestId2; - FunctionsResponse.Commitment s_requestCommitment2; - bytes32 s_requestId3; - FunctionsResponse.Commitment s_requestCommitment3; - - function setUp() public virtual override { - FunctionsFulfillmentSetup.setUp(); - // Make 2 additional requests (1 already complete) - - // *** Request #2 *** - vm.recordLogs(); - s_requestId2 = s_functionsClient.sendRequest( - s_donId, - "return 'hello world';", - new bytes(0), - new string[](0), - new bytes[](0), - s_subscriptionId, - 5500 - ); + /// @notice Gather signatures on report data + /// @param report - Report bytes generated from `_buildReport` + /// @param reportContext - Report context bytes32 generated from `_buildReport` + /// @param signerPrivateKeys - One or more addresses that will sign the report data + /// @return rawRs - Signature rs + /// @return rawSs - Signature ss + /// @return rawVs - Signature vs + function _signReport( + bytes memory report, + bytes32[3] memory reportContext, + uint256[] memory signerPrivateKeys + ) internal pure returns (bytes32[] memory rawRs, bytes32[] memory rawSs, bytes32 rawVs) { + bytes32[] memory rs = new bytes32[](signerPrivateKeys.length); + bytes32[] memory ss = new bytes32[](signerPrivateKeys.length); + bytes memory vs = new bytes(signerPrivateKeys.length); + + bytes32 reportDigest = keccak256(abi.encodePacked(keccak256(report), reportContext)); + + for (uint256 i = 0; i < signerPrivateKeys.length; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKeys[i], reportDigest); + rs[i] = r; + ss[i] = s; + vs[i] = bytes1(v - 27); + } + + return (rs, ss, bytes32(vs)); + } - // Get commitment data from OracleRequest event log - Vm.Log[] memory entriesAfterRequest2 = vm.getRecordedLogs(); - (, , , , , , , FunctionsResponse.Commitment memory commitment2) = abi.decode( - entriesAfterRequest2[0].data, - (address, uint64, address, bytes, uint16, bytes32, uint64, FunctionsResponse.Commitment) + /// @notice Provide a response from the DON to fulfill one or more requests and store the updated balances of the DON & Admin + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param transmitter - The address that will send the `.report` transaction + /// @param expectedToSucceed - Boolean representing if the report transmission is expected to produce a RequestProcessed event for every fulfillment. If not, we ignore retrieving the event log. + /// @param requestProcessedIndex - On a successful fulfillment the Router will emit a RequestProcessed event. To grab that event we must know the order at which this event was thrown in the report transmission lifecycle. This can change depending on the test setup (e.g. the Client contract gives an extra event during its callback) + /// @param transmitterGasToUse - Override the default amount of gas that the transmitter sends the `.report` transaction with + function _reportAndStore( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors, + address transmitter, + bool expectedToSucceed, + uint8 requestProcessedIndex, + uint256 transmitterGasToUse + ) internal { + { + if (requestNumberKeys.length != results.length || requestNumberKeys.length != errors.length) { + revert("_reportAndStore arguments length mismatch"); + } + } + + (bytes memory report, bytes32[3] memory reportContext) = _buildReport(requestNumberKeys, results, errors); + + // Sign the report + // Need at least 3 signers to fulfill minimum number of: (configInfo.n + configInfo.f) / 2 + 1 + uint256[] memory signerPrivateKeys = new uint256[](3); + signerPrivateKeys[0] = NOP_SIGNER_PRIVATE_KEY_1; + signerPrivateKeys[1] = NOP_SIGNER_PRIVATE_KEY_2; + signerPrivateKeys[2] = NOP_SIGNER_PRIVATE_KEY_3; + (bytes32[] memory rawRs, bytes32[] memory rawSs, bytes32 rawVs) = _signReport( + report, + reportContext, + signerPrivateKeys ); - s_requestCommitment2 = commitment2; - // Transmit as transmitter 2 + // Send as transmitter vm.stopPrank(); - vm.startPrank(NOP_TRANSMITTER_ADDRESS_2); - - // Build report - bytes32[] memory requestIds2 = new bytes32[](1); - requestIds2[0] = s_requestId2; - bytes[] memory results2 = new bytes[](1); - results2[0] = bytes("hello world!"); - bytes[] memory errors2 = new bytes[](1); - // No error - bytes[] memory onchainMetadata2 = new bytes[](1); - onchainMetadata2[0] = abi.encode(s_requestCommitment2); - bytes[] memory offchainMetadata2 = new bytes[](1); - // No offchain metadata - bytes memory report2 = abi.encode(requestIds2, results2, errors2, onchainMetadata2, offchainMetadata2); - - // Build signers - address[31] memory signers2; - signers2[0] = NOP_SIGNER_ADDRESS_2; + vm.startPrank(transmitter); // Send report vm.recordLogs(); - s_functionsCoordinator.callReportWithSigners(report2, signers2); - - // Get actual cost from RequestProcessed event log - Vm.Log[] memory entriesAfterFulfill2 = vm.getRecordedLogs(); - (uint96 totalCostJuels2, , , , , ) = abi.decode( - entriesAfterFulfill2[2].data, - (uint96, address, FunctionsResponse.FulfillResult, bytes, bytes, bytes) - ); - // totalCostJuels = costWithoutCallbackJuels + adminFee + callbackGasCostJuels - s_fulfillmentCoordinatorBalance += totalCostJuels2 - s_adminFee; - s_fulfillmentRouterOwnerBalance += s_adminFee; + if (transmitterGasToUse > 0) { + s_functionsCoordinator.transmit{gas: transmitterGasToUse}(reportContext, report, rawRs, rawSs, rawVs); + } else { + s_functionsCoordinator.transmit(reportContext, report, rawRs, rawSs, rawVs); + } + + if (expectedToSucceed) { + // Get actual cost from RequestProcessed event log + (uint96 totalCostJuels, , , , , ) = abi.decode( + vm.getRecordedLogs()[requestProcessedIndex].data, + (uint96, address, FunctionsResponse.FulfillResult, bytes, bytes, bytes) + ); + // Store profit amounts + s_fulfillmentRouterOwnerBalance += s_adminFee; + // totalCostJuels = costWithoutCallbackJuels + adminFee + callbackGasCostJuels + s_fulfillmentCoordinatorBalance += totalCostJuels - s_adminFee; + } // Return prank to Owner vm.stopPrank(); vm.startPrank(OWNER_ADDRESS); + } - // *** Request #3 *** - vm.recordLogs(); - s_requestId3 = s_functionsClient.sendRequest( - s_donId, - "return 'hello world';", - new bytes(0), - new string[](0), - new bytes[](0), - s_subscriptionId, - 5500 - ); + /// @notice Provide a response from the DON to fulfill one or more requests and store the updated balances of the DON & Admin + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param transmitter - The address that will send the `.report` transaction + /// @param expectedToSucceed - Boolean representing if the report transmission is expected to produce a RequestProcessed event for every fulfillment. If not, we ignore retrieving the event log. + /// @param requestProcessedIndex - On a successful fulfillment the Router will emit a RequestProcessed event. To grab that event we must know the order at which this event was thrown in the report transmission lifecycle. This can change depending on the test setup (e.g. the Client contract gives an extra event during its callback) + /// @dev @param transmitterGasToUse is overloaded to give transmitterGasToUse as 0] - Sends the `.report` transaction with the default amount of gas + function _reportAndStore( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors, + address transmitter, + bool expectedToSucceed, + uint8 requestProcessedIndex + ) internal { + _reportAndStore(requestNumberKeys, results, errors, transmitter, expectedToSucceed, requestProcessedIndex, 0); + } - // Get commitment data from OracleRequest event log - Vm.Log[] memory entriesAfterRequest3 = vm.getRecordedLogs(); - (, , , , , , , FunctionsResponse.Commitment memory commitment3) = abi.decode( - entriesAfterRequest3[0].data, - (address, uint64, address, bytes, uint16, bytes32, uint64, FunctionsResponse.Commitment) - ); - s_requestCommitment3 = commitment3; + /// @notice Provide a response from the DON to fulfill one or more requests and store the updated balances of the DON & Admin + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param transmitter - The address that will send the `.report` transaction + /// @param expectedToSucceed - Boolean representing if the report transmission is expected to produce a RequestProcessed event for every fulfillment. If not, we ignore retrieving the event log. + /// @dev @param requestProcessedIndex is overloaded to give requestProcessedIndex as 3 (happy path value)] - On a successful fulfillment the Router will emit a RequestProcessed event. To grab that event we must know the order at which this event was thrown in the report transmission lifecycle. This can change depending on the test setup (e.g. the Client contract gives an extra event during its callback) + /// @dev @param transmitterGasToUse is overloaded to give transmitterGasToUse as 0] - Sends the `.report` transaction with the default amount of gas + function _reportAndStore( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors, + address transmitter, + bool expectedToSucceed + ) internal { + _reportAndStore(requestNumberKeys, results, errors, transmitter, expectedToSucceed, 3); + } - // Transmit as transmitter 3 - vm.stopPrank(); - vm.startPrank(NOP_TRANSMITTER_ADDRESS_3); + /// @notice Provide a response from the DON to fulfill one or more requests and store the updated balances of the DON & Admin + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param transmitter - The address that will send the `.report` transaction + /// @dev @param expectedToSucceed is overloaded to give the value as true - The report transmission is expected to produce a RequestProcessed event for every fulfillment + /// @dev @param requestProcessedIndex is overloaded to give requestProcessedIndex as 3 (happy path value)] - On a successful fulfillment the Router will emit a RequestProcessed event. To grab that event we must know the order at which this event was thrown in the report transmission lifecycle. This can change depending on the test setup (e.g. the Client contract gives an extra event during its callback) + /// @dev @param transmitterGasToUse is overloaded to give transmitterGasToUse as 0] - Sends the `.report` transaction with the default amount of gas + function _reportAndStore( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors, + address transmitter + ) internal { + _reportAndStore(requestNumberKeys, results, errors, transmitter, true); + } - // Build report - bytes32[] memory requestIds3 = new bytes32[](1); - requestIds3[0] = s_requestId3; - bytes[] memory results3 = new bytes[](1); - results3[0] = bytes("hello world!"); - bytes[] memory errors3 = new bytes[](1); - // No error - bytes[] memory onchainMetadata3 = new bytes[](1); - onchainMetadata3[0] = abi.encode(s_requestCommitment3); - bytes[] memory offchainMetadata3 = new bytes[](1); - // No offchain metadata - bytes memory report3 = abi.encode(requestIds3, results3, errors3, onchainMetadata3, offchainMetadata3); - - // Build signers - address[31] memory signers3; - signers3[0] = NOP_SIGNER_ADDRESS_3; + /// @notice Provide a response from the DON to fulfill one or more requests and store the updated balances of the DON & Admin + /// @param requestNumberKeys - One or more requestNumberKeys that were used to store the request in `s_requests` of the requests, that will be added to the report + /// @param results - The result that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @param errors - The error that will be sent to the consumer contract's callback. For each index, e.g. result[index] or errors[index], only one of should be filled. + /// @dev @param transmitter is overloaded to give the value of transmitter #1 - The address that will send the `.report` transaction + /// @dev @param expectedToSucceed is overloaded to give the value as true - The report transmission is expected to produce a RequestProcessed event for every fulfillment + /// @dev @param requestProcessedIndex is overloaded to give requestProcessedIndex as 3 (happy path value)] - On a successful fulfillment the Router will emit a RequestProcessed event. To grab that event we must know the order at which this event was thrown in the report transmission lifecycle. This can change depending on the test setup (e.g. the Client contract gives an extra event during its callback) + /// @dev @param transmitterGasToUse is overloaded to give transmitterGasToUse as 0] - Sends the `.report` transaction with the default amount of gas + function _reportAndStore( + uint256[] memory requestNumberKeys, + string[] memory results, + bytes[] memory errors + ) internal { + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1); + } +} - // Send report - vm.recordLogs(); - s_functionsCoordinator.callReportWithSigners(report3, signers3); +/// @notice Set up to have transmitter #1 send a report that fulfills request #1 +contract FunctionsFulfillmentSetup is FunctionsClientRequestSetup { + function setUp() public virtual override { + FunctionsClientRequestSetup.setUp(); - // Get actual cost from RequestProcessed event log - Vm.Log[] memory entriesAfterFulfill3 = vm.getRecordedLogs(); - (uint96 totalCostJuels3, , , , , ) = abi.decode( - entriesAfterFulfill3[2].data, - (uint96, address, FunctionsResponse.FulfillResult, bytes, bytes, bytes) - ); + // Fulfill request 1 + uint256[] memory requestNumberKeys = new uint256[](1); + requestNumberKeys[0] = 1; + string[] memory results = new string[](1); + results[0] = "hello world!"; + bytes[] memory errors = new bytes[](1); + errors[0] = new bytes(0); - // totalCostJuels = costWithoutCallbackJuels + adminFee + callbackGasCostJuels - s_fulfillmentCoordinatorBalance += totalCostJuels3 - s_adminFee; + _reportAndStore(requestNumberKeys, results, errors, NOP_TRANSMITTER_ADDRESS_1, true); + } +} - // Return prank to Owner - vm.stopPrank(); - vm.startPrank(OWNER_ADDRESS); +/// @notice Set up to send and fulfill two more requests, s_request[2] reported by transmitter #2 and s_request[3] reported by transmitter #3 +contract FunctionsMultipleFulfillmentsSetup is FunctionsFulfillmentSetup { + function setUp() public virtual override { + FunctionsFulfillmentSetup.setUp(); + + // Make 2 additional requests (1 already complete) + + // *** Request #2 *** + // Send + string memory sourceCode = "return 'hello world';"; + bytes memory secrets = new bytes(0); + string[] memory args = new string[](0); + bytes[] memory bytesArgs = new bytes[](0); + uint32 callbackGasLimit = 5500; + _sendAndStoreRequest(2, sourceCode, secrets, args, bytesArgs, callbackGasLimit); + // Fulfill as transmitter #2 + uint256[] memory requestNumberKeys1 = new uint256[](1); + requestNumberKeys1[0] = 2; + string[] memory results1 = new string[](1); + results1[0] = "hello world!"; + bytes[] memory errors1 = new bytes[](1); + errors1[0] = new bytes(0); + _reportAndStore(requestNumberKeys1, results1, errors1, NOP_TRANSMITTER_ADDRESS_2, true); + + // *** Request #3 *** + // Send + _sendAndStoreRequest(3, sourceCode, secrets, args, bytesArgs, callbackGasLimit); + // Fulfill as transmitter #3 + uint256[] memory requestNumberKeys2 = new uint256[](1); + requestNumberKeys2[0] = 3; + string[] memory results2 = new string[](1); + results2[0] = "hello world!"; + bytes[] memory errors2 = new bytes[](1); + errors2[0] = new bytes(0); + _reportAndStore(requestNumberKeys2, results2, errors2, NOP_TRANSMITTER_ADDRESS_3, true); } } diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 9d741130502..ba8e242ea8e 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -178,7 +178,7 @@ func NewApplication(opts ApplicationOpts) (Application, error) { restrictedHTTPClient := opts.RestrictedHTTPClient unrestrictedHTTPClient := opts.UnrestrictedHTTPClient - // LOOPs can be be created as options, in the case of LOOP relayers, or + // LOOPs can be created as options, in the case of LOOP relayers, or // as OCR2 job implementations, in the case of Median today. // We will have a non-nil registry here in LOOP relayers are being used, otherwise // we need to initialize in case we serve OCR2 LOOPs @@ -217,8 +217,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { globalLogger.Info("Nurse service (automatic pprof profiling) is disabled") } - healthChecker := services.NewChecker() - telemetryIngressClient := synchronization.TelemetryIngressClient(&synchronization.NoopTelemetryIngressClient{}) telemetryIngressBatchClient := synchronization.TelemetryIngressBatchClient(&synchronization.NoopTelemetryIngressBatchClient{}) monitoringEndpointGen := telemetry.MonitoringEndpointGenerator(&telemetry.NoopAgent{}) @@ -421,29 +419,40 @@ func NewApplication(opts ApplicationOpts) (Application, error) { } } - var feedsService feeds.Service + var feedsService feeds.Service = &feeds.NullService{} if cfg.Feature().FeedsManager() { - feedsORM := feeds.NewORM(db, opts.Logger, cfg.Database()) - feedsService = feeds.NewService( - feedsORM, - jobORM, - db, - jobSpawner, - keyStore, - cfg.Insecure(), - cfg.JobPipeline(), - cfg.OCR(), - cfg.OCR2(), - cfg.Database(), - legacyEVMChains, - globalLogger, - opts.Version, - ) - } else { - feedsService = &feeds.NullService{} + if keys, err := opts.KeyStore.CSA().GetAll(); err != nil { + globalLogger.Warn("[Feeds Service] Unable to start without CSA key", "err", err) + } else if len(keys) == 0 { + globalLogger.Warn("[Feeds Service] Unable to start without CSA key") + } else { + feedsORM := feeds.NewORM(db, opts.Logger, cfg.Database()) + feedsService = feeds.NewService( + feedsORM, + jobORM, + db, + jobSpawner, + keyStore, + cfg.Insecure(), + cfg.JobPipeline(), + cfg.OCR(), + cfg.OCR2(), + cfg.Database(), + legacyEVMChains, + globalLogger, + opts.Version, + ) + } } - app := &ChainlinkApplication{ + healthChecker := services.NewChecker() + for _, s := range srvcs { + if err := healthChecker.Register(s); err != nil { + return nil, err + } + } + + return &ChainlinkApplication{ relayers: opts.RelayerChainInteroperators, EventBroadcaster: eventBroadcaster, jobORM: jobORM, @@ -472,27 +481,7 @@ func NewApplication(opts ApplicationOpts) (Application, error) { // NOTE: Can keep things clean by putting more things in srvcs instead of manually start/closing srvcs: srvcs, - } - - for _, service := range app.srvcs { - checkable := service.(services.Checkable) - if err := app.HealthChecker.Register(service.Name(), checkable); err != nil { - return nil, err - } - } - - // To avoid subscribing chain services twice, we only subscribe them if OCR2 is not enabled. - // If it's enabled, they are going to be registered with relayers by default. - if !cfg.OCR2().Enabled() { - for _, service := range app.relayers.Services() { - checkable := service.(services.Checkable) - if err := app.HealthChecker.Register(service.Name(), checkable); err != nil { - return nil, err - } - } - } - - return app, nil + }, nil } func (app *ChainlinkApplication) SetLogLevel(lvl zapcore.Level) error { diff --git a/core/services/checkable.go b/core/services/checkable.go index 298e3dda471..a702f9f979d 100644 --- a/core/services/checkable.go +++ b/core/services/checkable.go @@ -10,4 +10,6 @@ type Checkable interface { // HealthReport returns a full health report of the callee including it's dependencies. // key is the dep name, value is nil if healthy, or error message otherwise. HealthReport() map[string]error + // Name returns the fully qualified name of the component. Usually the logger name. + Name() string } diff --git a/core/services/health.go b/core/services/health.go index da3a99b34d9..1912a49b75e 100644 --- a/core/services/health.go +++ b/core/services/health.go @@ -21,7 +21,7 @@ type ( // Checker provides a service which can be probed for system health. Checker interface { // Register a service for health checks. - Register(name string, service Checkable) error + Register(service Checkable) error // Unregister a service. Unregister(name string) error // IsReady returns the current readiness of the system. @@ -176,8 +176,9 @@ func (c *checker) update() { uptimeSeconds.Add(interval.Seconds()) } -func (c *checker) Register(name string, service Checkable) error { - if service == nil || name == "" { +func (c *checker) Register(service Checkable) error { + name := service.Name() + if name == "" { return errors.Errorf("misconfigured check %#v for %v", name, service) } diff --git a/core/services/health_test.go b/core/services/health_test.go index 295eb86119c..f8e139af157 100644 --- a/core/services/health_test.go +++ b/core/services/health_test.go @@ -1,7 +1,6 @@ package services_test import ( - "fmt" "net/http" "testing" "time" @@ -22,6 +21,8 @@ type boolCheck struct { healthy bool } +func (b boolCheck) Name() string { return b.name } + func (b boolCheck) Ready() error { if b.healthy { return nil @@ -57,8 +58,8 @@ func TestCheck(t *testing.T) { }}, } { c := services.NewChecker() - for i, check := range test.checks { - require.NoError(t, c.Register(fmt.Sprint(i), check)) + for _, check := range test.checks { + require.NoError(t, c.Register(check)) } require.NoError(t, c.Start()) diff --git a/core/services/mocks/checker.go b/core/services/mocks/checker.go index cfccc2537f8..8a6541bba36 100644 --- a/core/services/mocks/checker.go +++ b/core/services/mocks/checker.go @@ -78,13 +78,13 @@ func (_m *Checker) IsReady() (bool, map[string]error) { return r0, r1 } -// Register provides a mock function with given fields: name, service -func (_m *Checker) Register(name string, service services.Checkable) error { - ret := _m.Called(name, service) +// Register provides a mock function with given fields: service +func (_m *Checker) Register(service services.Checkable) error { + ret := _m.Called(service) var r0 error - if rf, ok := ret.Get(0).(func(string, services.Checkable) error); ok { - r0 = rf(name, service) + if rf, ok := ret.Get(0).(func(services.Checkable) error); ok { + r0 = rf(service) } else { r0 = ret.Error(0) } diff --git a/core/services/service.go b/core/services/service.go index b3c1bf36779..5bf61e062d2 100644 --- a/core/services/service.go +++ b/core/services/service.go @@ -81,8 +81,5 @@ type ServiceCtx interface { // again, you need to build a new Service to do so. Close() error - // Name returns the fully qualified name of the service - Name() string - Checkable } diff --git a/plugins/cmd/chainlink-median/main.go b/plugins/cmd/chainlink-median/main.go index aac68eb5005..4d966548525 100644 --- a/plugins/cmd/chainlink-median/main.go +++ b/plugins/cmd/chainlink-median/main.go @@ -20,7 +20,7 @@ func main() { p := median.NewPlugin(s.Logger) defer s.Logger.ErrorIfFn(p.Close, "Failed to close") - s.MustRegister(p.Name(), p) + s.MustRegister(p) stop := make(chan struct{}) defer close(stop) diff --git a/plugins/cmd/chainlink-solana/main.go b/plugins/cmd/chainlink-solana/main.go index df2824fb338..132d7244fdf 100644 --- a/plugins/cmd/chainlink-solana/main.go +++ b/plugins/cmd/chainlink-solana/main.go @@ -27,7 +27,7 @@ func main() { p := &pluginRelayer{Base: plugins.Base{Logger: s.Logger}} defer s.Logger.ErrorIfFn(p.Close, "Failed to close") - s.MustRegister(p.Name(), p) + s.MustRegister(p) stopCh := make(chan struct{}) defer close(stopCh) diff --git a/plugins/cmd/chainlink-starknet/main.go b/plugins/cmd/chainlink-starknet/main.go index 5015f70be2e..aa69c85fe42 100644 --- a/plugins/cmd/chainlink-starknet/main.go +++ b/plugins/cmd/chainlink-starknet/main.go @@ -27,7 +27,7 @@ func main() { p := &pluginRelayer{Base: plugins.Base{Logger: s.Logger}} defer s.Logger.ErrorIfFn(p.Close, "Failed to close") - s.MustRegister(p.Name(), p) + s.MustRegister(p) stopCh := make(chan struct{}) defer close(stopCh) diff --git a/plugins/server.go b/plugins/server.go index 2c1bffb85b8..0d0e0dc62c4 100644 --- a/plugins/server.go +++ b/plugins/server.go @@ -53,10 +53,9 @@ type Server struct { } // MustRegister registers the Checkable with services.Checker, or exits upon failure. -func (s *Server) MustRegister(name string, c services.Checkable) { - err := s.Register(name, c) - if err != nil { - s.Logger.Fatalf("Failed to register %s with health checker: %v", name, err) +func (s *Server) MustRegister(c services.Checkable) { + if err := s.Register(c); err != nil { + s.Logger.Fatalf("Failed to register %s with health checker: %v", c.Name(), err) } }