diff --git a/common/client/mock_rpc_test.go b/common/client/mock_rpc_test.go index d87a02d47c1..72c6eb19029 100644 --- a/common/client/mock_rpc_test.go +++ b/common/client/mock_rpc_test.go @@ -426,6 +426,36 @@ func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS return r0, r1 } +// PendingCallContract provides a mock function with given fields: ctx, msg +func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD]) PendingCallContract(ctx context.Context, msg interface{}) ([]byte, error) { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for PendingCallContract") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, interface{}) ([]byte, error)); ok { + return rf(ctx, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, interface{}) []byte); ok { + r0 = rf(ctx, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, interface{}) error); ok { + r1 = rf(ctx, msg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PendingSequenceAt provides a mock function with given fields: ctx, addr func (_m *mockRPC[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD]) PendingSequenceAt(ctx context.Context, addr ADDR) (SEQ, error) { ret := _m.Called(ctx, addr) diff --git a/common/client/multi_node.go b/common/client/multi_node.go index ed1a2700c71..ae9b3afd0d4 100644 --- a/common/client/multi_node.go +++ b/common/client/multi_node.go @@ -465,6 +465,17 @@ func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OP return n.RPC().CallContract(ctx, attempt, blockNumber) } +func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, RPC_CLIENT]) PendingCallContract( + ctx context.Context, + attempt interface{}, +) (rpcErr []byte, extractErr error) { + n, err := c.selectNode() + if err != nil { + return rpcErr, err + } + return n.RPC().PendingCallContract(ctx, attempt) +} + // ChainID makes a direct RPC call. In most cases it should be better to use the configured chain id instead by // calling ConfiguredChainID. func (c *multiNode[CHAIN_ID, SEQ, ADDR, BLOCK_HASH, TX, TX_HASH, EVENT, EVENT_OPS, TX_RECEIPT, FEE, HEAD, RPC_CLIENT]) ChainID(ctx context.Context) (id CHAIN_ID, err error) { diff --git a/common/client/types.go b/common/client/types.go index 32d4da98b50..fe9e4d7d482 100644 --- a/common/client/types.go +++ b/common/client/types.go @@ -124,6 +124,10 @@ type clientAPI[ msg interface{}, blockNumber *big.Int, ) (rpcErr []byte, extractErr error) + PendingCallContract( + ctx context.Context, + msg interface{}, + ) (rpcErr []byte, extractErr error) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error CodeAt(ctx context.Context, account ADDR, blockNumber *big.Int) ([]byte, error) } diff --git a/core/chains/evm/client/chain_client.go b/core/chains/evm/client/chain_client.go index 2a5a37da47c..b16054b69a8 100644 --- a/core/chains/evm/client/chain_client.go +++ b/core/chains/evm/client/chain_client.go @@ -130,6 +130,10 @@ func (c *chainClient) CallContract(ctx context.Context, msg ethereum.CallMsg, bl return c.multiNode.CallContract(ctx, msg, blockNumber) } +func (c *chainClient) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + return c.multiNode.PendingCallContract(ctx, msg) +} + // TODO-1663: change this to actual ChainID() call once client.go is deprecated. func (c *chainClient) ChainID() (*big.Int, error) { //return c.multiNode.ChainID(ctx), nil diff --git a/core/chains/evm/client/client.go b/core/chains/evm/client/client.go index 61635c59c6b..e2ae8c26403 100644 --- a/core/chains/evm/client/client.go +++ b/core/chains/evm/client/client.go @@ -91,6 +91,7 @@ type Client interface { HeaderByHash(ctx context.Context, h common.Hash) (*types.Header, error) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) IsL2() bool } @@ -260,6 +261,10 @@ func (client *client) CallContract(ctx context.Context, msg ethereum.CallMsg, bl return client.pool.CallContract(ctx, msg, blockNumber) } +func (client *client) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + return client.pool.PendingCallContract(ctx, msg) +} + func (client *client) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { return client.pool.CodeAt(ctx, account, blockNumber) } diff --git a/core/chains/evm/client/erroring_node.go b/core/chains/evm/client/erroring_node.go index c33891728a7..059f76d608a 100644 --- a/core/chains/evm/client/erroring_node.go +++ b/core/chains/evm/client/erroring_node.go @@ -103,6 +103,10 @@ func (e *erroringNode) CallContract(ctx context.Context, msg ethereum.CallMsg, b return nil, errors.New(e.errMsg) } +func (e *erroringNode) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + return nil, errors.New(e.errMsg) +} + func (e *erroringNode) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { return nil, errors.New(e.errMsg) } diff --git a/core/chains/evm/client/mocks/client.go b/core/chains/evm/client/mocks/client.go index 0b45894cf28..bbaaafd7615 100644 --- a/core/chains/evm/client/mocks/client.go +++ b/core/chains/evm/client/mocks/client.go @@ -585,6 +585,36 @@ func (_m *Client) NodeStates() map[string]string { return r0 } +// PendingCallContract provides a mock function with given fields: ctx, msg +func (_m *Client) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for PendingCallContract") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) ([]byte, error)); ok { + return rf(ctx, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) []byte); ok { + r0 = rf(ctx, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { + r1 = rf(ctx, msg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PendingCodeAt provides a mock function with given fields: ctx, account func (_m *Client) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { ret := _m.Called(ctx, account) diff --git a/core/chains/evm/client/node.go b/core/chains/evm/client/node.go index a27321535ed..aa472d605a6 100644 --- a/core/chains/evm/client/node.go +++ b/core/chains/evm/client/node.go @@ -117,6 +117,7 @@ type Node interface { EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) SuggestGasPrice(ctx context.Context) (*big.Int, error) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) + PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) HeaderByHash(context.Context, common.Hash) (*types.Header, error) @@ -830,6 +831,33 @@ func (n *node) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumb } +func (n *node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) (val []byte, err error) { + ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) + if err != nil { + return nil, err + } + defer cancel() + lggr := n.newRqLggr().With("callMsg", msg) + + lggr.Debug("RPC call: evmclient.Client#PendingCallContract") + start := time.Now() + if http != nil { + val, err = http.geth.PendingCallContract(ctx, msg) + err = n.wrapHTTP(err) + } else { + val, err = ws.geth.PendingCallContract(ctx, msg) + err = n.wrapWS(err) + } + duration := time.Since(start) + + n.logResult(lggr, err, duration, n.getRPCDomain(), "PendingCallContract", + "val", val, + ) + + return + +} + func (n *node) BlockByNumber(ctx context.Context, number *big.Int) (b *types.Block, err error) { ctx, cancel, ws, http, err := n.makeLiveQueryCtxAndSafeGetClients(ctx) if err != nil { diff --git a/core/chains/evm/client/null_client.go b/core/chains/evm/client/null_client.go index e3bb1defd0d..3cbae9e9dde 100644 --- a/core/chains/evm/client/null_client.go +++ b/core/chains/evm/client/null_client.go @@ -196,6 +196,11 @@ func (nc *NullClient) CallContract(ctx context.Context, msg ethereum.CallMsg, bl return nil, nil } +func (nc *NullClient) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + nc.lggr.Debug("PendingCallContract") + return nil, nil +} + func (nc *NullClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { nc.lggr.Debug("CodeAt") return nil, nil diff --git a/core/chains/evm/client/pool.go b/core/chains/evm/client/pool.go index b2d5a4847a5..3c33b3dbd0a 100644 --- a/core/chains/evm/client/pool.go +++ b/core/chains/evm/client/pool.go @@ -477,6 +477,10 @@ func (p *Pool) CallContract(ctx context.Context, msg ethereum.CallMsg, blockNumb return p.selectNode().CallContract(ctx, msg, blockNumber) } +func (p *Pool) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + return p.selectNode().PendingCallContract(ctx, msg) +} + func (p *Pool) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { return p.selectNode().CodeAt(ctx, account, blockNumber) } diff --git a/core/chains/evm/client/rpc_client.go b/core/chains/evm/client/rpc_client.go index ce3a67162ed..54656cf1d3e 100644 --- a/core/chains/evm/client/rpc_client.go +++ b/core/chains/evm/client/rpc_client.go @@ -792,6 +792,34 @@ func (r *rpcClient) CallContract(ctx context.Context, msg interface{}, blockNumb } +func (r *rpcClient) PendingCallContract(ctx context.Context, msg interface{}) (val []byte, err error) { + ctx, cancel, ws, http, err := r.makeLiveQueryCtxAndSafeGetClients(ctx) + if err != nil { + return nil, err + } + defer cancel() + lggr := r.newRqLggr().With("callMsg", msg) + message := msg.(ethereum.CallMsg) + + lggr.Debug("RPC call: evmclient.Client#PendingCallContract") + start := time.Now() + if http != nil { + val, err = http.geth.PendingCallContract(ctx, message) + err = r.wrapHTTP(err) + } else { + val, err = ws.geth.PendingCallContract(ctx, message) + err = r.wrapWS(err) + } + duration := time.Since(start) + + r.logResult(lggr, err, duration, r.getRPCDomain(), "PendingCallContract", + "val", val, + ) + + return + +} + func (r *rpcClient) LatestBlockHeight(ctx context.Context) (*big.Int, error) { var height big.Int h, err := r.BlockNumber(ctx) diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index bd2e959d9bc..c49637e7890 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -102,6 +102,8 @@ func (c *SimulatedBackendClient) CallContext(ctx context.Context, result interfa return c.ethCall(ctx, result, args...) case "eth_getHeaderByNumber": return c.ethGetHeaderByNumber(ctx, result, args...) + case "eth_estimateGas": + return c.ethEstimateGas(ctx, result, args...) default: return fmt.Errorf("second arg to SimulatedBackendClient.Call is an RPC API method which has not yet been implemented: %s. Add processing for it here", method) } @@ -401,6 +403,25 @@ func (c *SimulatedBackendClient) CallContract(ctx context.Context, msg ethereum. return res, nil } +func (c *SimulatedBackendClient) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + // Expected error is + // type JsonError struct { + // Code int `json:"code"` + // Message string `json:"message"` + // Data interface{} `json:"data,omitempty"` + //} + res, err := c.b.PendingCallContract(ctx, msg) + if err != nil { + dataErr := revertError{} + if errors.Is(err, &dataErr) { + return nil, &JsonError{Data: dataErr.ErrorData(), Message: dataErr.Error(), Code: 3} + } + // Generic revert, no data + return nil, &JsonError{Data: []byte{}, Message: err.Error(), Code: 3} + } + return res, nil +} + // CodeAt gets the code associated with an account as of a specified block. func (c *SimulatedBackendClient) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { return c.b.CodeAt(ctx, account, blockNumber) @@ -443,6 +464,8 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B b[i].Error = c.ethCall(ctx, b[i].Result, b[i].Args...) case "eth_getHeaderByNumber": b[i].Error = c.ethGetHeaderByNumber(ctx, b[i].Result, b[i].Args...) + case "eth_estimateGas": + b[i].Error = c.ethEstimateGas(ctx, b[i].Result, b[i].Args...) default: return fmt.Errorf("SimulatedBackendClient got unsupported method %s", elem.Method) } @@ -562,6 +585,37 @@ func (c *SimulatedBackendClient) ethGetBlockByNumber(ctx context.Context, result return nil } +func (c *SimulatedBackendClient) ethEstimateGas(ctx context.Context, result interface{}, args ...interface{}) error { + if len(args) != 2 { + return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_estimateGas", len(args)) + } + + params, ok := args[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("SimulatedBackendClient expected first arg to be map[string]interface{} for eth_call, got: %T", args[0]) + } + + _, err := c.blockNumber(args[1]) + if err != nil { + return fmt.Errorf("SimulatedBackendClient expected second arg to be the string 'latest' or a *big.Int for eth_call, got: %T", args[1]) + } + + resp, err := c.b.EstimateGas(ctx, toCallMsg(params)) + if err != nil { + return err + } + + switch typedResult := result.(type) { + case *uint64: + *typedResult = resp + case *hexutil.Uint64: + *typedResult = hexutil.Uint64(resp) + default: + return fmt.Errorf("SimulatedBackendClient unexpected type %T", result) + } + + return nil +} func (c *SimulatedBackendClient) ethCall(ctx context.Context, result interface{}, args ...interface{}) error { if len(args) != 2 { @@ -625,7 +679,6 @@ func (c *SimulatedBackendClient) ethGetHeaderByNumber(ctx context.Context, resul func toCallMsg(params map[string]interface{}) ethereum.CallMsg { var callMsg ethereum.CallMsg - toAddr, err := interfaceToAddress(params["to"]) if err != nil { panic(fmt.Errorf("unexpected 'to' parameter: %s", err)) @@ -645,6 +698,10 @@ func toCallMsg(params map[string]interface{}) ethereum.CallMsg { callMsg.From = common.HexToAddress("0x") } + if params["data"] != nil && params["input"] != nil { + panic("cannot have both 'data' and 'input' parameters") + } + switch data := params["data"].(type) { case nil: // This parameter is not required so nil is acceptable @@ -656,16 +713,41 @@ func toCallMsg(params map[string]interface{}) ethereum.CallMsg { panic("unexpected type of 'data' parameter; try hexutil.Bytes, []byte, or nil") } + switch input := params["input"].(type) { + case nil: + // This parameter is not required so nil is acceptable + case hexutil.Bytes: + callMsg.Data = input + case []byte: + callMsg.Data = input + default: + panic("unexpected type of 'input' parameter; try hexutil.Bytes, []byte, or nil") + } + if value, ok := params["value"].(*big.Int); ok { callMsg.Value = value } - if gas, ok := params["gas"].(uint64); ok { + switch gas := params["gas"].(type) { + case nil: + // This parameter is not required so nil is acceptable + case uint64: callMsg.Gas = gas + case hexutil.Uint64: + callMsg.Gas = uint64(gas) + default: + panic("unexpected type of 'gas' parameter; try hexutil.Uint64, or uint64") } - if gasPrice, ok := params["gasPrice"].(*big.Int); ok { + switch gasPrice := params["gasPrice"].(type) { + case nil: + // This parameter is not required so nil is acceptable + case *big.Int: callMsg.GasPrice = gasPrice + case *hexutil.Big: + callMsg.GasPrice = gasPrice.ToInt() + default: + panic("unexpected type of 'gasPrice' parameter; try *big.Int, or *hexutil.Big") } return callMsg @@ -675,6 +757,11 @@ func interfaceToAddress(value interface{}) (common.Address, error) { switch v := value.(type) { case common.Address: return v, nil + case *common.Address: + if v == nil { + return common.Address{}, nil + } + return *v, nil case string: if ok := common.IsHexAddress(v); !ok { return common.Address{}, fmt.Errorf("string not formatted as a hex encoded evm address") @@ -688,6 +775,6 @@ func interfaceToAddress(value interface{}) (common.Address, error) { return common.BigToAddress(v), nil default: - return common.Address{}, fmt.Errorf("unrecognized value type for converting value to common.Address; use hex encoded string, *big.Int, or common.Address") + return common.Address{}, fmt.Errorf("unrecognized value type: %T for converting value to common.Address; use hex encoded string, *big.Int, or common.Address", v) } } diff --git a/core/chains/evm/mocks/node.go b/core/chains/evm/mocks/node.go index 8f27218aec7..25944cfcf42 100644 --- a/core/chains/evm/mocks/node.go +++ b/core/chains/evm/mocks/node.go @@ -496,6 +496,36 @@ func (_m *Node) Order() int32 { return r0 } +// PendingCallContract provides a mock function with given fields: ctx, msg +func (_m *Node) PendingCallContract(ctx context.Context, msg ethereum.CallMsg) ([]byte, error) { + ret := _m.Called(ctx, msg) + + if len(ret) == 0 { + panic("no return value specified for PendingCallContract") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) ([]byte, error)); ok { + return rf(ctx, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) []byte); ok { + r0 = rf(ctx, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ethereum.CallMsg) error); ok { + r1 = rf(ctx, msg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // PendingCodeAt provides a mock function with given fields: ctx, account func (_m *Node) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { ret := _m.Called(ctx, account) diff --git a/core/scripts/common/vrf/jobs/jobs.go b/core/scripts/common/vrf/jobs/jobs.go index 40b7732b49f..571530c15c9 100644 --- a/core/scripts/common/vrf/jobs/jobs.go +++ b/core/scripts/common/vrf/jobs/jobs.go @@ -27,7 +27,8 @@ vrf [type=vrfv2 estimate_gas [type=estimategaslimit to="%s" multiplier="%f" - data="$(vrf.output)"] + data="$(vrf.output)" + block="%s"] simulate [type=ethcall from="%s" to="%s" @@ -35,7 +36,8 @@ simulate [type=ethcall gasPrice="$(jobSpec.maxGasPrice)" extractRevertReason=true contract="%s" - data="$(vrf.output)"] + data="$(vrf.output)" + block="%s"] decode_log->vrf->estimate_gas->simulate """` @@ -66,7 +68,8 @@ generate_proof [type=vrfv2plus estimate_gas [type=estimategaslimit to="%s" multiplier="%f" - data="$(generate_proof.output)"] + data="$(generate_proof.output)" + block="%s"] simulate_fulfillment [type=ethcall from="%s" to="%s" @@ -74,7 +77,8 @@ simulate_fulfillment [type=ethcall gasPrice="$(jobSpec.maxGasPrice)" extractRevertReason=true contract="%s" - data="$(generate_proof.output)"] + data="$(generate_proof.output)" + block="%s"] decode_log->generate_proof->estimate_gas->simulate_fulfillment """ ` diff --git a/core/scripts/common/vrf/setup-envs/main.go b/core/scripts/common/vrf/setup-envs/main.go index dfeed0ddbe9..251d7b08079 100644 --- a/core/scripts/common/vrf/setup-envs/main.go +++ b/core/scripts/common/vrf/setup-envs/main.go @@ -87,6 +87,7 @@ func main() { subscriptionBalanceNativeWeiString := flag.String("subscription-balance-native", constants.SubscriptionBalanceNativeWei, "amount to fund subscription with native token (Wei)") minConfs := flag.Int("min-confs", constants.MinConfs, "minimum confirmations") + nativeOnly := flag.Bool("native-only", false, "if true, link and link feed are not set up. Only used in v2 plus") linkAddress := flag.String("link-address", "", "address of link token") linkEthAddress := flag.String("link-eth-feed", "", "address of link eth feed") bhsContractAddressString := flag.String("bhs-address", "", "address of BHS contract") @@ -104,6 +105,7 @@ func main() { flatFeeLinkDiscountPPM := flag.Int64("flat-fee-link-discount-ppm", 100, "fulfillment flat fee discount for LINK payment denominated in native ppm") nativePremiumPercentage := flag.Int64("native-premium-percentage", 1, "premium percentage for native payment") linkPremiumPercentage := flag.Int64("link-premium-percentage", 1, "premium percentage for LINK payment") + simulationBlock := flag.String("simulation-block", "pending", "simulation block can be 'pending' or 'latest'") e := helpers.SetupEnv(false) flag.Parse() @@ -114,6 +116,10 @@ func main() { } fmt.Println("Using VRF Version:", *vrfVersion) + if *simulationBlock != "pending" && *simulationBlock != "latest" { + helpers.PanicErr(fmt.Errorf("simulation block must be 'pending' or 'latest'")) + } + fundingAmount := decimal.RequireFromString(*nodeSendingKeyFundingAmount).BigInt() subscriptionBalanceJuels := decimal.RequireFromString(*subscriptionBalanceJuelsString).BigInt() subscriptionBalanceNativeWei := decimal.RequireFromString(*subscriptionBalanceNativeWeiString).BigInt() @@ -247,6 +253,7 @@ func main() { coordinatorJobSpecConfig, bhsJobSpecConfig, *useTestCoordinator, + *simulationBlock, ) case "v2plus": coordinatorConfigV2Plus := v2plusscripts.CoordinatorConfigV2Plus{ @@ -281,10 +288,12 @@ func main() { vrfKeyRegistrationConfig, contractAddresses, coordinatorConfigV2Plus, + *nativeOnly, nodesMap, provingKeyMaxGasPrice.Uint64(), coordinatorJobSpecConfig, bhsJobSpecConfig, + *simulationBlock, ) } diff --git a/core/scripts/vrfv2/testnet/v2scripts/super_scripts.go b/core/scripts/vrfv2/testnet/v2scripts/super_scripts.go index 1266d2c068c..b4d29af3aee 100644 --- a/core/scripts/vrfv2/testnet/v2scripts/super_scripts.go +++ b/core/scripts/vrfv2/testnet/v2scripts/super_scripts.go @@ -64,6 +64,7 @@ func DeployUniverseViaCLI(e helpers.Environment) { deployVRFOwner := deployCmd.Bool("deploy-vrf-owner", true, "whether to deploy VRF owner contracts") useTestCoordinator := deployCmd.Bool("use-test-coordinator", true, "whether to use test coordinator") + simulationBlock := deployCmd.String("simulation-block", "pending", "simulation block can be 'pending' or 'latest'") // optional flags fallbackWeiPerUnitLinkString := deployCmd.String("fallback-wei-per-unit-link", constants.FallbackWeiPerUnitLink.String(), "fallback wei/link ratio") @@ -87,6 +88,10 @@ func DeployUniverseViaCLI(e helpers.Environment) { reqsForTier4 := deployCmd.Int64("reqs-for-tier-4", constants.ReqsForTier4, "requests for tier 4") reqsForTier5 := deployCmd.Int64("reqs-for-tier-5", constants.ReqsForTier5, "requests for tier 5") + if *simulationBlock != "pending" && *simulationBlock != "latest" { + helpers.PanicErr(fmt.Errorf("simulation block must be 'pending' or 'latest'")) + } + helpers.ParseArgs( deployCmd, os.Args[2:], ) @@ -174,6 +179,7 @@ func DeployUniverseViaCLI(e helpers.Environment) { coordinatorJobSpecConfig, bhsJobSpecConfig, *useTestCoordinator, + *simulationBlock, ) vrfPrimaryNode := nodesMap[model.VRFPrimaryNodeName] @@ -194,6 +200,7 @@ func VRFV2DeployUniverse( coordinatorJobSpecConfig model.CoordinatorJobSpecConfig, bhsJobSpecConfig model.BHSJobSpecConfig, useTestCoordinator bool, + simulationBlock string, ) model.JobSpecs { var compressedPkHex string var keyHash common.Hash @@ -360,6 +367,7 @@ func VRFV2DeployUniverse( coordinatorJobSpecConfig.RequestTimeout, //requestTimeout contractAddresses.CoordinatorAddress, coordinatorJobSpecConfig.EstimateGasMultiplier, //estimateGasMultiplier + simulationBlock, func() string { if keys := nodesMap[model.VRFPrimaryNodeName].SendingKeys; len(keys) > 0 { return keys[0].Address @@ -368,6 +376,7 @@ func VRFV2DeployUniverse( }(), contractAddresses.CoordinatorAddress, contractAddresses.CoordinatorAddress, + simulationBlock, ) if deployVRFOwner { formattedVrfPrimaryJobSpec = strings.Replace(formattedVrfPrimaryJobSpec, @@ -391,6 +400,7 @@ func VRFV2DeployUniverse( coordinatorJobSpecConfig.RequestTimeout, //requestTimeout contractAddresses.CoordinatorAddress, coordinatorJobSpecConfig.EstimateGasMultiplier, //estimateGasMultiplier + simulationBlock, func() string { if keys := nodesMap[model.VRFPrimaryNodeName].SendingKeys; len(keys) > 0 { return keys[0].Address @@ -399,6 +409,7 @@ func VRFV2DeployUniverse( }(), contractAddresses.CoordinatorAddress, contractAddresses.CoordinatorAddress, + simulationBlock, ) if deployVRFOwner { formattedVrfBackupJobSpec = strings.Replace(formattedVrfBackupJobSpec, diff --git a/core/scripts/vrfv2plus/testnet/v2plusscripts/super_scripts.go b/core/scripts/vrfv2plus/testnet/v2plusscripts/super_scripts.go index 3daadc61ef7..12c08dd80a3 100644 --- a/core/scripts/vrfv2plus/testnet/v2plusscripts/super_scripts.go +++ b/core/scripts/vrfv2plus/testnet/v2plusscripts/super_scripts.go @@ -472,6 +472,7 @@ func DeployUniverseViaCLI(e helpers.Environment) { deployCmd := flag.NewFlagSet("deploy-universe", flag.ExitOnError) // required flags + nativeOnly := deployCmd.Bool("native-only", false, "if true, link and link feed are not set up") linkAddress := deployCmd.String("link-address", "", "address of link token") linkEthAddress := deployCmd.String("link-eth-feed", "", "address of link eth feed") bhsContractAddressString := deployCmd.String("bhs-address", "", "address of BHS contract") @@ -490,6 +491,7 @@ func DeployUniverseViaCLI(e helpers.Environment) { bhsJobLookBackBlocks := flag.Int("bhs-job-look-back-blocks", 200, "") bhsJobPollPeriod := flag.String("bhs-job-poll-period", "3s", "") bhsJobRunTimeout := flag.String("bhs-job-run-timeout", "1m", "") + simulationBlock := deployCmd.String("simulation-block", "pending", "simulation block can be 'pending' or 'latest'") // optional flags fallbackWeiPerUnitLinkString := deployCmd.String("fallback-wei-per-unit-link", "6e16", "fallback wei/link ratio") @@ -511,6 +513,19 @@ func DeployUniverseViaCLI(e helpers.Environment) { deployCmd, os.Args[2:], ) + if *nativeOnly { + if *linkAddress != "" || *linkEthAddress != "" { + panic("native-only flag is set, but link address or link eth address is provided") + } + if *subscriptionBalanceJuelsString != "0" { + panic("native-only flag is set, but link subscription balance is provided") + } + } + + if *simulationBlock != "pending" && *simulationBlock != "latest" { + helpers.PanicErr(fmt.Errorf("simulation block must be 'pending' or 'latest'")) + } + fallbackWeiPerUnitLink := decimal.RequireFromString(*fallbackWeiPerUnitLinkString).BigInt() subscriptionBalanceJuels := decimal.RequireFromString(*subscriptionBalanceJuelsString).BigInt() subscriptionBalanceNativeWei := decimal.RequireFromString(*subscriptionBalanceNativeWeiString).BigInt() @@ -581,10 +596,12 @@ func DeployUniverseViaCLI(e helpers.Environment) { vrfKeyRegistrationConfig, contractAddresses, coordinatorConfig, + *nativeOnly, nodesMap, provingKeyMaxGasPrice.Uint64(), coordinatorJobSpecConfig, bhsJobSpecConfig, + *simulationBlock, ) vrfPrimaryNode := nodesMap[model.VRFPrimaryNodeName] @@ -600,10 +617,12 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, vrfKeyRegistrationConfig model.VRFKeyRegistrationConfig, contractAddresses model.ContractAddresses, coordinatorConfig CoordinatorConfigV2Plus, + nativeOnly bool, nodesMap map[string]model.Node, provingKeyMaxGasPrice uint64, coordinatorJobSpecConfig model.CoordinatorJobSpecConfig, bhsJobSpecConfig model.BHSJobSpecConfig, + simulationBlock string, ) model.JobSpecs { var compressedPkHex string var keyHash common.Hash @@ -632,12 +651,12 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, helpers.PanicErr(err) } - if len(contractAddresses.LinkAddress) == 0 { + if !nativeOnly && len(contractAddresses.LinkAddress) == 0 { fmt.Println("\nDeploying LINK Token...") contractAddresses.LinkAddress = helpers.DeployLinkToken(e).String() } - if len(contractAddresses.LinkEthAddress) == 0 { + if !nativeOnly && len(contractAddresses.LinkEthAddress) == 0 { fmt.Println("\nDeploying LINK/ETH Feed...") contractAddresses.LinkEthAddress = helpers.DeployLinkEthFeed(e, contractAddresses.LinkAddress, coordinatorConfig.FallbackWeiPerUnitLink).String() } @@ -741,6 +760,7 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, coordinatorJobSpecConfig.RequestTimeout, //requestTimeout contractAddresses.CoordinatorAddress, coordinatorJobSpecConfig.EstimateGasMultiplier, //estimateGasMultiplier + simulationBlock, func() string { if keys := nodesMap[model.VRFPrimaryNodeName].SendingKeys; len(keys) > 0 { return keys[0].Address @@ -749,6 +769,7 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, }(), contractAddresses.CoordinatorAddress, contractAddresses.CoordinatorAddress, + simulationBlock, ) formattedVrfV2PlusBackupJobSpec := fmt.Sprintf( @@ -765,6 +786,7 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, coordinatorJobSpecConfig.RequestTimeout, //requestTimeout contractAddresses.CoordinatorAddress, coordinatorJobSpecConfig.EstimateGasMultiplier, //estimateGasMultiplier + simulationBlock, func() string { if keys := nodesMap[model.VRFPrimaryNodeName].SendingKeys; len(keys) > 0 { return keys[0].Address @@ -773,6 +795,7 @@ func VRFV2PlusDeployUniverse(e helpers.Environment, }(), contractAddresses.CoordinatorAddress, contractAddresses.CoordinatorAddress, + simulationBlock, ) formattedBHSJobSpec := fmt.Sprintf( diff --git a/core/scripts/vrfv2plus/testnet/v2plusscripts/util.go b/core/scripts/vrfv2plus/testnet/v2plusscripts/util.go index 213a2f0dcff..716e0058ff4 100644 --- a/core/scripts/vrfv2plus/testnet/v2plusscripts/util.go +++ b/core/scripts/vrfv2plus/testnet/v2plusscripts/util.go @@ -53,10 +53,12 @@ func DeployCoordinator( coordinator, err := vrf_coordinator_v2_5.NewVRFCoordinatorV25(coordinatorAddress, e.Ec) helpers.PanicErr(err) - linkTx, err := coordinator.SetLINKAndLINKNativeFeed(e.Owner, - common.HexToAddress(linkAddress), common.HexToAddress(linkEthAddress)) - helpers.PanicErr(err) - helpers.ConfirmTXMined(context.Background(), e.Ec, linkTx, e.ChainID) + if linkAddress != "" && linkEthAddress != "" { + linkTx, err := coordinator.SetLINKAndLINKNativeFeed(e.Owner, + common.HexToAddress(linkAddress), common.HexToAddress(linkEthAddress)) + helpers.PanicErr(err) + helpers.ConfirmTXMined(context.Background(), e.Ec, linkTx, e.ChainID) + } return coordinatorAddress } diff --git a/core/services/pipeline/common.go b/core/services/pipeline/common.go index da0e0d3c00b..6e7ad1e7e4e 100644 --- a/core/services/pipeline/common.go +++ b/core/services/pipeline/common.go @@ -681,3 +681,14 @@ func getJsonNumberValue(value json.Number) (interface{}, error) { return result, nil } + +func selectBlock(block string) (string, error) { + if block == "" { + return "latest", nil + } + block = strings.ToLower(block) + if block == "pending" || block == "latest" { + return block, nil + } + return "", pkgerrors.Errorf("unsupported block param: %s", block) +} diff --git a/core/services/pipeline/task.estimategas.go b/core/services/pipeline/task.estimategas.go index 43c148b287f..8fccc5e8eac 100644 --- a/core/services/pipeline/task.estimategas.go +++ b/core/services/pipeline/task.estimategas.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/shopspring/decimal" "go.uber.org/multierr" @@ -28,6 +29,7 @@ type EstimateGasLimitTask struct { Multiplier string `json:"multiplier"` Data string `json:"data"` EVMChainID string `json:"evmChainID" mapstructure:"evmChainID"` + Block string `json:"block"` specGasLimit *uint32 legacyChains legacyevm.LegacyChainContainer @@ -61,6 +63,7 @@ func (t *EstimateGasLimitTask) Run(ctx context.Context, lggr logger.Logger, vars data BytesParam multiplier DecimalParam chainID StringParam + block StringParam ) err := multierr.Combine( errors.Wrap(ResolveParam(&fromAddr, From(VarExpr(t.From, vars), utils.ZeroAddress)), "from"), @@ -69,6 +72,7 @@ func (t *EstimateGasLimitTask) Run(ctx context.Context, lggr logger.Logger, vars // Default to 1, i.e. exactly what estimateGas suggests errors.Wrap(ResolveParam(&multiplier, From(VarExpr(t.Multiplier, vars), NonemptyString(t.Multiplier), decimal.New(1, 0))), "multiplier"), errors.Wrap(ResolveParam(&chainID, From(VarExpr(t.getEvmChainID(), vars), NonemptyString(t.getEvmChainID()), "")), "evmChainID"), + errors.Wrap(ResolveParam(&block, From(VarExpr(t.Block, vars), t.Block)), "block"), ) if err != nil { return Result{Error: err}, runInfo @@ -82,18 +86,32 @@ func (t *EstimateGasLimitTask) Run(ctx context.Context, lggr logger.Logger, vars maximumGasLimit := SelectGasLimit(chain.Config().EVM().GasEstimator(), t.jobType, t.specGasLimit) to := common.Address(toAddr) - gasLimit, err := chain.Client().EstimateGas(ctx, ethereum.CallMsg{ - From: common.Address(fromAddr), - To: &to, - Data: data, - }) + var gasLimit hexutil.Uint64 + args := map[string]interface{}{ + "from": common.Address(fromAddr), + "to": &to, + "input": hexutil.Bytes([]byte(data)), + } + + selectedBlock, err := selectBlock(string(block)) + if err != nil { + return Result{Error: err}, runInfo + } + err = chain.Client().CallContext(ctx, + &gasLimit, + "eth_estimateGas", + args, + selectedBlock, + ) + if err != nil { // Fallback to the maximum conceivable gas limit // if we're unable to call estimate gas for whatever reason. lggr.Warnw("EstimateGas: unable to estimate, fallback to configured limit", "err", err, "fallback", maximumGasLimit) return Result{Value: maximumGasLimit}, runInfo } - gasLimitDecimal, err := decimal.NewFromString(strconv.FormatUint(gasLimit, 10)) + + gasLimitDecimal, err := decimal.NewFromString(strconv.FormatUint(uint64(gasLimit), 10)) if err != nil { return Result{Error: err}, retryableRunInfo() } diff --git a/core/services/pipeline/task.eth_call.go b/core/services/pipeline/task.eth_call.go index 56b2df08c4e..f011cd7a9b6 100644 --- a/core/services/pipeline/task.eth_call.go +++ b/core/services/pipeline/task.eth_call.go @@ -3,6 +3,7 @@ package pipeline import ( "context" "fmt" + "strings" "time" "github.com/ethereum/go-ethereum" @@ -33,6 +34,7 @@ type ETHCallTask struct { GasUnlimited string `json:"gasUnlimited"` ExtractRevertReason bool `json:"extractRevertReason"` EVMChainID string `json:"evmChainID" mapstructure:"evmChainID"` + Block string `json:"block"` specGasLimit *uint32 legacyChains legacyevm.LegacyChainContainer @@ -78,6 +80,7 @@ func (t *ETHCallTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, in gasFeeCap MaybeBigIntParam gasUnlimited BoolParam chainID StringParam + block StringParam ) err = multierr.Combine( errors.Wrap(ResolveParam(&contractAddr, From(VarExpr(t.Contract, vars), NonemptyString(t.Contract))), "contract"), @@ -89,6 +92,7 @@ func (t *ETHCallTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, in errors.Wrap(ResolveParam(&gasFeeCap, From(VarExpr(t.GasFeeCap, vars), t.GasFeeCap)), "gasFeeCap"), errors.Wrap(ResolveParam(&chainID, From(VarExpr(t.getEvmChainID(), vars), NonemptyString(t.getEvmChainID()), "")), "evmChainID"), errors.Wrap(ResolveParam(&gasUnlimited, From(VarExpr(t.GasUnlimited, vars), NonemptyString(t.GasUnlimited), false)), "gasUnlimited"), + errors.Wrap(ResolveParam(&block, From(VarExpr(t.Block, vars), t.Block)), "block"), ) if err != nil { return Result{Error: err}, runInfo @@ -131,7 +135,15 @@ func (t *ETHCallTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, in With("gasFeeCap", call.GasFeeCap) start := time.Now() - resp, err := chain.Client().CallContract(ctx, call, nil) + + var resp []byte + blockStr := block.String() + if blockStr == "" || strings.ToLower(blockStr) == "latest" { + resp, err = chain.Client().CallContract(ctx, call, nil) + } else if strings.ToLower(blockStr) == "pending" { + resp, err = chain.Client().PendingCallContract(ctx, call) + } + elapsed := time.Since(start) if err != nil { if t.ExtractRevertReason { @@ -149,6 +161,5 @@ func (t *ETHCallTask) Run(ctx context.Context, lggr logger.Logger, vars Vars, in } promETHCallTime.WithLabelValues(t.DotID()).Set(float64(elapsed)) - return Result{Value: resp}, runInfo } diff --git a/core/services/pipeline/task.eth_call_test.go b/core/services/pipeline/task.eth_call_test.go index cb58a03a9df..28af94ba25c 100644 --- a/core/services/pipeline/task.eth_call_test.go +++ b/core/services/pipeline/task.eth_call_test.go @@ -43,6 +43,7 @@ func TestETHCallTask(t *testing.T) { data string evmChainID string gas string + block string specGasLimit *uint32 vars pipeline.Vars inputs []pipeline.Result @@ -58,6 +59,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -78,6 +80,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "$(gasLimit)", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -99,6 +102,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", &specGasLimit, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -119,6 +123,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -140,6 +145,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -155,6 +161,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -170,6 +177,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "zork": []byte("foo bar"), @@ -185,6 +193,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte(nil), @@ -200,6 +209,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "0", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -215,6 +225,7 @@ func TestETHCallTask(t *testing.T) { "$(foo)", "$(evmChainID)", "", + "", nil, pipeline.NewVarsFrom(map[string]interface{}{ "foo": []byte("foo bar"), @@ -229,6 +240,48 @@ func TestETHCallTask(t *testing.T) { }, nil, nil, chains.ErrNoSuchChainID.Error(), }, + { + "simulate using latest block", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + "", + "$(foo)", + "0", + "", + "latest", + nil, + pipeline.NewVarsFrom(map[string]interface{}{ + "foo": []byte("foo bar"), + }), + nil, + func(ethClient *evmclimocks.Client, config *pipelinemocks.Config) { + contractAddr := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF") + ethClient. + On("CallContract", mock.Anything, ethereum.CallMsg{To: &contractAddr, Gas: uint64(drJobTypeGasLimit), Data: []byte("foo bar")}, (*big.Int)(nil)). + Return([]byte("baz quux"), nil) + }, + []byte("baz quux"), nil, "", + }, + { + "simulate using pending block", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", + "", + "$(foo)", + "0", + "", + "pending", + nil, + pipeline.NewVarsFrom(map[string]interface{}{ + "foo": []byte("foo bar"), + }), + nil, + func(ethClient *evmclimocks.Client, config *pipelinemocks.Config) { + contractAddr := common.HexToAddress("0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF") + ethClient. + On("PendingCallContract", mock.Anything, ethereum.CallMsg{To: &contractAddr, Gas: uint64(drJobTypeGasLimit), Data: []byte("foo bar")}). + Return([]byte("baz quux"), nil) + }, + []byte("baz quux"), nil, "", + }, } for _, test := range tests { @@ -241,6 +294,7 @@ func TestETHCallTask(t *testing.T) { Data: test.data, EVMChainID: test.evmChainID, Gas: test.gas, + Block: test.block, } ethClient := evmclimocks.NewClient(t) diff --git a/core/services/pipeline/task.vrfv2plus.go b/core/services/pipeline/task.vrfv2plus.go index ff9f96e7eca..6bc299eeb6f 100644 --- a/core/services/pipeline/task.vrfv2plus.go +++ b/core/services/pipeline/task.vrfv2plus.go @@ -138,6 +138,7 @@ func (t *VRFTaskV2Plus) Run(_ context.Context, lggr logger.Logger, vars Vars, in return Result{Error: err}, retryableRunInfo() } // onlyPremium is false because this task assumes that chainlink node fulfills the VRF request + // gas cost should be billed to the requesting subscription b, err := vrfCoordinatorV2PlusABI.Pack("fulfillRandomWords", onChainProof, rc, false /* onlyPremium */) if err != nil { return Result{Error: err}, runInfo diff --git a/core/services/vrf/vrfcommon/validate.go b/core/services/vrf/vrfcommon/validate.go index 07e8676ddb7..09f2e669483 100644 --- a/core/services/vrf/vrfcommon/validate.go +++ b/core/services/vrf/vrfcommon/validate.go @@ -47,9 +47,6 @@ func ValidatedVRFSpec(tomlString string) (job.Job, error) { if bytes.Equal(spec.PublicKey[:], empty[:]) { return jb, errors.Wrap(ErrKeyNotSet, "publicKey") } - if spec.MinIncomingConfirmations == 0 { - return jb, errors.Wrap(ErrKeyNotSet, "minIncomingConfirmations") - } if spec.CoordinatorAddress.String() == "" { return jb, errors.Wrap(ErrKeyNotSet, "coordinatorAddress") } diff --git a/core/testdata/testspecs/v2_specs.go b/core/testdata/testspecs/v2_specs.go index a78f5d8530c..5ca79f1abb4 100644 --- a/core/testdata/testspecs/v2_specs.go +++ b/core/testdata/testspecs/v2_specs.go @@ -376,6 +376,7 @@ estimate_gas [type=estimategaslimit to="%s" multiplier="1.1" data="$(generate_proof.output)" + block="latest" ] simulate_fulfillment [type=ethcall to="%s" @@ -384,6 +385,7 @@ simulate_fulfillment [type=ethcall extractRevertReason=true contract="%s" data="$(generate_proof.output)" + block="latest" ] decode_log->generate_proof->estimate_gas->simulate_fulfillment `, coordinatorAddress, coordinatorAddress, coordinatorAddress) diff --git a/integration-tests/actions/vrf/common/models.go b/integration-tests/actions/vrf/common/models.go index bb486bd598b..ab6ca034800 100644 --- a/integration-tests/actions/vrf/common/models.go +++ b/integration-tests/actions/vrf/common/models.go @@ -66,4 +66,5 @@ type VRFJobSpecConfig struct { PollPeriod time.Duration RequestTimeout time.Duration VRFOwnerConfig *VRFOwnerConfig + SimulationBlock *string } diff --git a/integration-tests/actions/vrf/vrfv2/vrfv2_steps.go b/integration-tests/actions/vrf/vrfv2/vrfv2_steps.go index 453ff205faa..55b41f21856 100644 --- a/integration-tests/actions/vrf/vrfv2/vrfv2_steps.go +++ b/integration-tests/actions/vrf/vrfv2/vrfv2_steps.go @@ -170,6 +170,7 @@ func CreateVRFV2Job( Address: vrfJobSpecConfig.CoordinatorAddress, EstimateGasMultiplier: vrfJobSpecConfig.EstimateGasMultiplier, FromAddress: vrfJobSpecConfig.FromAddresses[0], + SimulationBlock: vrfJobSpecConfig.SimulationBlock, } ost, err := os.String() if err != nil { @@ -394,6 +395,7 @@ func setupVRFNode(contracts *vrfcommon.VRFContracts, chainID *big.Int, vrfv2Conf BatchFulfillmentGasMultiplier: *vrfv2Config.VRFJobBatchFulfillmentGasMultiplier, PollPeriod: vrfv2Config.VRFJobPollPeriod.Duration, RequestTimeout: vrfv2Config.VRFJobRequestTimeout.Duration, + SimulationBlock: vrfv2Config.VRFJobSimulationBlock, VRFOwnerConfig: vrfOwnerConfig, } diff --git a/integration-tests/actions/vrf/vrfv2plus/vrfv2plus_steps.go b/integration-tests/actions/vrf/vrfv2plus/vrfv2plus_steps.go index 16203f7c0a0..0268b958e40 100644 --- a/integration-tests/actions/vrf/vrfv2plus/vrfv2plus_steps.go +++ b/integration-tests/actions/vrf/vrfv2plus/vrfv2plus_steps.go @@ -89,6 +89,7 @@ func CreateVRFV2PlusJob( Address: vrfJobSpecConfig.CoordinatorAddress, EstimateGasMultiplier: vrfJobSpecConfig.EstimateGasMultiplier, FromAddress: vrfJobSpecConfig.FromAddresses[0], + SimulationBlock: vrfJobSpecConfig.SimulationBlock, } ost, err := os.String() if err != nil { @@ -331,6 +332,7 @@ func setupVRFNode(contracts *vrfcommon.VRFContracts, chainID *big.Int, config *v BatchFulfillmentGasMultiplier: *config.VRFJobBatchFulfillmentGasMultiplier, PollPeriod: config.VRFJobPollPeriod.Duration, RequestTimeout: config.VRFJobRequestTimeout.Duration, + SimulationBlock: config.VRFJobSimulationBlock, VRFOwnerConfig: nil, } diff --git a/integration-tests/client/chainlink_models.go b/integration-tests/client/chainlink_models.go index 370497423f3..0e144d3ab39 100644 --- a/integration-tests/client/chainlink_models.go +++ b/integration-tests/client/chainlink_models.go @@ -627,11 +627,23 @@ func (d *PipelineSpec) String() (string, error) { return MarshallTemplate(d, "API call pipeline template", sourceString) } +func getOptionalSimBlock(simBlock *string) (string, error) { + optionalSimBlock := "" + if simBlock != nil { + if *simBlock != "latest" && *simBlock != "pending" { + return "", fmt.Errorf("invalid simulation block value: %s", *simBlock) + } + optionalSimBlock = fmt.Sprintf("block=\"%s\"", *simBlock) + } + return optionalSimBlock, nil +} + // VRFV2TxPipelineSpec VRFv2 request with tx callback type VRFV2PlusTxPipelineSpec struct { Address string EstimateGasMultiplier float64 FromAddress string + SimulationBlock *string // can be nil, "latest" or "pending". } // Type returns the type of the pipeline @@ -641,7 +653,11 @@ func (d *VRFV2PlusTxPipelineSpec) Type() string { // String representation of the pipeline func (d *VRFV2PlusTxPipelineSpec) String() (string, error) { - sourceString := ` + optionalSimBlock, err := getOptionalSimBlock(d.SimulationBlock) + if err != nil { + return "", err + } + sourceTemplate := ` decode_log [type=ethabidecodelog abi="RandomWordsRequested(bytes32 indexed keyHash,uint256 requestId,uint256 preSeed,uint256 indexed subId,uint16 minimumRequestConfirmations,uint32 callbackGasLimit,uint32 numWords,bytes extraArgs,address indexed sender)" data="$(jobRun.logData)" @@ -654,7 +670,8 @@ generate_proof [type=vrfv2plus estimate_gas [type=estimategaslimit to="{{ .Address }}" multiplier="{{ .EstimateGasMultiplier }}" - data="$(generate_proof.output)"] + data="$(generate_proof.output)" + %s] simulate_fulfillment [type=ethcall from="{{ .FromAddress }}" to="{{ .Address }}" @@ -662,8 +679,11 @@ simulate_fulfillment [type=ethcall gasPrice="$(jobSpec.maxGasPrice)" extractRevertReason=true contract="{{ .Address }}" - data="$(generate_proof.output)"] + data="$(generate_proof.output)" + %s] decode_log->generate_proof->estimate_gas->simulate_fulfillment` + + sourceString := fmt.Sprintf(sourceTemplate, optionalSimBlock, optionalSimBlock) return MarshallTemplate(d, "VRFV2 Plus pipeline template", sourceString) } @@ -672,6 +692,7 @@ type VRFV2TxPipelineSpec struct { Address string EstimateGasMultiplier float64 FromAddress string + SimulationBlock *string // can be nil, "latest" or "pending". } // Type returns the type of the pipeline @@ -681,7 +702,11 @@ func (d *VRFV2TxPipelineSpec) Type() string { // String representation of the pipeline func (d *VRFV2TxPipelineSpec) String() (string, error) { - sourceString := ` + optionalSimBlock, err := getOptionalSimBlock(d.SimulationBlock) + if err != nil { + return "", err + } + sourceTemplate := ` decode_log [type=ethabidecodelog abi="RandomWordsRequested(bytes32 indexed keyHash,uint256 requestId,uint256 preSeed,uint64 indexed subId,uint16 minimumRequestConfirmations,uint32 callbackGasLimit,uint32 numWords,address indexed sender)" data="$(jobRun.logData)" @@ -694,7 +719,8 @@ vrf [type=vrfv2 estimate_gas [type=estimategaslimit to="{{ .Address }}" multiplier="{{ .EstimateGasMultiplier }}" - data="$(vrf.output)"] + data="$(vrf.output)" + %s] simulate [type=ethcall from="{{ .FromAddress }}" to="{{ .Address }}" @@ -702,8 +728,11 @@ simulate [type=ethcall gasPrice="$(jobSpec.maxGasPrice)" extractRevertReason=true contract="{{ .Address }}" - data="$(vrf.output)"] + data="$(vrf.output)" + %s] decode_log->vrf->estimate_gas->simulate` + + sourceString := fmt.Sprintf(sourceTemplate, optionalSimBlock, optionalSimBlock) return MarshallTemplate(d, "VRFV2 pipeline template", sourceString) } diff --git a/integration-tests/smoke/vrfv2plus_test.go b/integration-tests/smoke/vrfv2plus_test.go index 29cc5534791..e13f12b2a43 100644 --- a/integration-tests/smoke/vrfv2plus_test.go +++ b/integration-tests/smoke/vrfv2plus_test.go @@ -1140,3 +1140,92 @@ func TestVRFV2PlusWithBHS(t *testing.T) { require.Equal(t, 0, randomWordsRequestedEvent.Raw.BlockHash.Cmp(randRequestBlockHash)) }) } + +func TestVRFv2PlusPendingBlockSimulationAndZeroConfirmationDelays(t *testing.T) { + t.Parallel() + l := logging.GetTestLogger(t) + + config, err := tc.GetConfig("Smoke", tc.VRFv2Plus) + if err != nil { + t.Fatal(err) + } + + // override config with minConf = 0 and use pending block for simulation + config.VRFv2Plus.General.MinimumConfirmations = ptr.Ptr[uint16](0) + config.VRFv2Plus.General.VRFJobSimulationBlock = ptr.Ptr[string]("pending") + + network, err := actions.EthereumNetworkConfigFromConfig(l, &config) + require.NoError(t, err, "Error building ethereum network config") + + env, err := test_env.NewCLTestEnvBuilder(). + WithTestInstance(t). + WithTestConfig(&config). + WithPrivateEthereumNetwork(network). + WithCLNodes(1). + WithFunding(big.NewFloat(*config.Common.ChainlinkNodeFunding)). + WithStandardCleanup(). + Build() + require.NoError(t, err, "error creating test env") + + env.ParallelTransactions(true) + + mockETHLinkFeed, err := actions.DeployMockETHLinkFeed(env.ContractDeployer, big.NewInt(*config.VRFv2Plus.General.LinkNativeFeedResponse)) + require.NoError(t, err, "error deploying mock ETH/LINK feed") + + linkToken, err := actions.DeployLINKToken(env.ContractDeployer) + require.NoError(t, err, "error deploying LINK contract") + + numberOfTxKeysToCreate := 2 + vrfv2PlusContracts, subIDs, vrfv2PlusData, nodesMap, err := vrfv2plus.SetupVRFV2_5Environment( + env, + []vrfcommon.VRFNodeType{vrfcommon.VRF}, + &config, + linkToken, + mockETHLinkFeed, + numberOfTxKeysToCreate, + 1, + 1, + l, + ) + require.NoError(t, err, "error setting up VRF v2_5 env") + + subID := subIDs[0] + + subscription, err := vrfv2PlusContracts.CoordinatorV2Plus.GetSubscription(testcontext.Get(t), subID) + require.NoError(t, err, "error getting subscription information") + + vrfv2plus.LogSubDetails(l, subscription, subID, vrfv2PlusContracts.CoordinatorV2Plus) + + var isNativeBilling = false + + jobRunsBeforeTest, err := env.ClCluster.Nodes[0].API.MustReadRunsByJob(nodesMap[vrfcommon.VRF].Job.Data.ID) + require.NoError(t, err, "error reading job runs") + + l.Info().Uint16("minimumConfirmationDelay", *config.VRFv2Plus.General.MinimumConfirmations).Msg("Minimum Confirmation Delay") + + // test and assert + randomWordsFulfilledEvent, err := vrfv2plus.RequestRandomnessAndWaitForFulfillment( + vrfv2PlusContracts.VRFV2PlusConsumer[0], + vrfv2PlusContracts.CoordinatorV2Plus, + vrfv2PlusData, + subID, + isNativeBilling, + *config.VRFv2Plus.General.MinimumConfirmations, + *config.VRFv2Plus.General.CallbackGasLimit, + *config.VRFv2Plus.General.NumberOfWords, + *config.VRFv2Plus.General.RandomnessRequestCountPerRequest, + *config.VRFv2Plus.General.RandomnessRequestCountPerRequestDeviation, + config.VRFv2Plus.General.RandomWordsFulfilledEventTimeout.Duration, + l, + ) + require.NoError(t, err, "error requesting randomness and waiting for fulfilment") + + jobRuns, err := env.ClCluster.Nodes[0].API.MustReadRunsByJob(nodesMap[vrfcommon.VRF].Job.Data.ID) + require.NoError(t, err, "error reading job runs") + require.Equal(t, len(jobRunsBeforeTest.Data)+1, len(jobRuns.Data)) + + status, err := vrfv2PlusContracts.VRFV2PlusConsumer[0].GetRequestStatus(testcontext.Get(t), randomWordsFulfilledEvent.RequestId) + require.NoError(t, err, "error getting rand request status") + require.True(t, status.Fulfilled) + l.Debug().Bool("Fulfilment Status", status.Fulfilled).Msg("Random Words Request Fulfilment Status") +} diff --git a/integration-tests/testconfig/common/vrf/common.go b/integration-tests/testconfig/common/vrf/common.go index 0929c7a8d01..da0733e9c4c 100644 --- a/integration-tests/testconfig/common/vrf/common.go +++ b/integration-tests/testconfig/common/vrf/common.go @@ -179,6 +179,7 @@ type General struct { VRFJobBatchFulfillmentGasMultiplier *float64 `toml:"vrf_job_batch_fulfillment_gas_multiplier"` VRFJobPollPeriod *blockchain.StrDuration `toml:"vrf_job_poll_period"` VRFJobRequestTimeout *blockchain.StrDuration `toml:"vrf_job_request_timeout"` + VRFJobSimulationBlock *string `toml:"vrf_job_simulation_block"` //BHS Job Config BHSJobWaitBlocks *int `toml:"bhs_job_wait_blocks"` @@ -277,6 +278,10 @@ func (c *General) Validate() error { return errors.New("vrf_job_request_timeout must be set to a non-negative value") } + if c.VRFJobSimulationBlock != nil && (*c.VRFJobSimulationBlock != "latest" && *c.VRFJobSimulationBlock != "pending") { + return errors.New("simulation_block must be nil or \"latest\" or \"pending\"") + } + if c.BHSJobLookBackBlocks == nil || *c.BHSJobLookBackBlocks < 0 { return errors.New("bhs_job_lookback_blocks must be set to a non-negative value") }