From 139ad08bf082537ac45c193e87f7e5fa481ae277 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:10:20 -0700 Subject: [PATCH 01/33] Add node api wrapper for ergonomic cmd usage --- .../keystone/src/02_deploy_jobspecs_cmd.go | 87 ++--------- core/scripts/keystone/src/99_app.go | 141 ++++++++++++++++++ 2 files changed, 150 insertions(+), 78 deletions(-) diff --git a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go index 275943d638..abc8b64160 100644 --- a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go +++ b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go @@ -1,21 +1,14 @@ package src import ( - "bytes" "errors" "flag" "fmt" "os" - "reflect" - "runtime" - "strings" - - "github.com/urfave/cli" helpers "github.com/smartcontractkit/chainlink/core/scripts/common" - "github.com/smartcontractkit/chainlink/v2/core/cmd" ) - +// Could be useful https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/scripts/chaincli/handler/scrape_node_config.go#L102 type deployJobSpecs struct{} func NewDeployJobSpecsCommand() *deployJobSpecs { @@ -79,87 +72,25 @@ func (g *deployJobSpecs) Run(args []string) { } for i, n := range nodes { - output := &bytes.Buffer{} - client, app := newApp(n, output) - fmt.Println("Logging in:", n.url) - loginFs := flag.NewFlagSet("test", flag.ContinueOnError) - loginFs.Bool("bypass-version-check", true, "") - loginCtx := cli.NewContext(app, loginFs, nil) - err := client.RemoteLogin(loginCtx) - helpers.PanicErr(err) - output.Reset() - + api := newNodeAPI(n) if !*onlyReplay { specToDeploy := flattenedSpecs[i].spec.ToString() specFragment := flattenedSpecs[i].spec[0:1] fmt.Printf("Deploying jobspec: %s\n... \n", specFragment) - fs := flag.NewFlagSet("test", flag.ExitOnError) - err = fs.Parse([]string{specToDeploy}) - helpers.PanicErr(err) - err = client.CreateJob(cli.NewContext(app, fs, nil)) + _, err := api.withArg(specToDeploy).exec(api.methods.CreateJob) if err != nil { fmt.Println("Failed to deploy job spec:", specFragment, "Error:", err) } - output.Reset() } - replayFs := flag.NewFlagSet("test", flag.ExitOnError) - flagSetApplyFromAction(client.ReplayFromBlock, replayFs, "") - err = replayFs.Set("block-number", fmt.Sprint(deployedContracts.SetConfigTxBlock)) - helpers.PanicErr(err) - err = replayFs.Set("evm-chain-id", fmt.Sprint(*chainID)) - helpers.PanicErr(err) - fmt.Printf("Replaying from block: %d\n", deployedContracts.SetConfigTxBlock) fmt.Printf("EVM Chain ID: %d\n\n", *chainID) - replayCtx := cli.NewContext(app, replayFs, nil) - err = client.ReplayFromBlock(replayCtx) - helpers.PanicErr(err) - } -} - -// flagSetApplyFromAction applies the flags from action to the flagSet. -// -// `parentCommand` will filter the app commands and only applies the flags if the command/subcommand has a parent with that name, if left empty no filtering is done -// -// Taken from: https://github.com/smartcontractkit/chainlink/blob/develop/core/cmd/shell_test.go#L590 -func flagSetApplyFromAction(action interface{}, flagSet *flag.FlagSet, parentCommand string) { - cliApp := cmd.Shell{} - app := cmd.NewApp(&cliApp) - - foundName := parentCommand == "" - actionFuncName := getFuncName(action) - - for _, command := range app.Commands { - flags := recursiveFindFlagsWithName(actionFuncName, command, parentCommand, foundName) - - for _, flag := range flags { - flag.Apply(flagSet) - } - } -} - -func recursiveFindFlagsWithName(actionFuncName string, command cli.Command, parent string, foundName bool) []cli.Flag { - if command.Action != nil { - if actionFuncName == getFuncName(command.Action) && foundName { - return command.Flags - } - } - - for _, subcommand := range command.Subcommands { - if !foundName { - foundName = strings.EqualFold(subcommand.Name, parent) - } - - found := recursiveFindFlagsWithName(actionFuncName, subcommand, parent, foundName) - if found != nil { - return found - } + api.withFlags(api.methods.ReplayFromBlock, func(fs *flag.FlagSet) { + err = fs.Set("block-number", fmt.Sprint(deployedContracts.SetConfigTxBlock)) + helpers.PanicErr(err) + err = fs.Set("evm-chain-id", fmt.Sprint(*chainID)) + helpers.PanicErr(err) + }).mustExec() } - return nil -} - -func getFuncName(i interface{}) string { - return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() } diff --git a/core/scripts/keystone/src/99_app.go b/core/scripts/keystone/src/99_app.go index 6e59932aa7..9a8fbad9e9 100644 --- a/core/scripts/keystone/src/99_app.go +++ b/core/scripts/keystone/src/99_app.go @@ -1,11 +1,20 @@ package src import ( + "bytes" + "encoding/json" + "errors" "flag" + "fmt" "io" + "reflect" + "runtime" + "strings" "github.com/urfave/cli" + "github.com/smartcontractkit/chainlink/v2/core/cmd" + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" clcmd "github.com/smartcontractkit/chainlink/v2/core/cmd" ) @@ -29,3 +38,135 @@ func newApp(n *node, writer io.Writer) (*clcmd.Shell, *cli.App) { client.Renderer = clcmd.RendererJSON{Writer: writer} return client, app } + +type nodeAPI struct { + methods *cmd.Shell + app *cli.App + output *bytes.Buffer + fs *flag.FlagSet + clientMethod func(*cli.Context) error +} + +func newNodeAPI(n *node) *nodeAPI { + output := &bytes.Buffer{} + methods, app := newApp(n, output) + + api := &nodeAPI{ + output: output, + methods: methods, + app: app, + fs: flag.NewFlagSet("test", flag.ContinueOnError), + } + + api.withFlags(api.methods.RemoteLogin, + func(fs *flag.FlagSet) { + fs.Set("bypass-version-check", fmt.Sprint(true)) + }).mustExec() + + return api +} + +func (c *nodeAPI) withArg(arg string) *nodeAPI { + + err := c.fs.Parse([]string{arg}) + helpers.PanicErr(err) + + return c +} + +func (c *nodeAPI) withArgs(args ...string) *nodeAPI { + err := c.fs.Parse(args) + helpers.PanicErr(err) + + return c +} + +func (c *nodeAPI) withFlags(clientMethod func(*cli.Context) error, applyFlags func(*flag.FlagSet)) *nodeAPI { + flagSetApplyFromAction(clientMethod, c.fs, "") + applyFlags(c.fs) + + c.clientMethod = clientMethod + + return c +} + +func (c *nodeAPI) exec(clientMethod ...func(*cli.Context) error) ([]byte, error) { + if len(clientMethod) > 1 { + PanicErr(errors.New("Only one client method allowed")) + } + + c.output.Reset() + defer c.output.Reset() + defer func() { c.fs = flag.NewFlagSet("test", flag.ContinueOnError) }() + + if c.clientMethod == nil { + c.clientMethod = clientMethod[0] + } + ctx := cli.NewContext(c.app, c.fs, nil) + err := c.clientMethod(ctx) + if err != nil { + return nil, err + } + + return c.output.Bytes(), nil +} + +func (c *nodeAPI) mustExec(clientMethod ...func(*cli.Context) error) []byte { + bytes, err := c.exec(clientMethod...) + helpers.PanicErr(err) + return bytes +} + +// flagSetApplyFromAction applies the flags from action to the flagSet. +// +// `parentCommand` will filter the app commands and only applies the flags if the command/subcommand has a parent with that name, if left empty no filtering is done +// +// Taken from: https://github.com/smartcontractkit/chainlink/blob/develop/core/cmd/shell_test.go#L590 +func flagSetApplyFromAction(action interface{}, flagSet *flag.FlagSet, parentCommand string) { + cliApp := cmd.Shell{} + app := cmd.NewApp(&cliApp) + + foundName := parentCommand == "" + actionFuncName := getFuncName(action) + + for _, command := range app.Commands { + flags := recursiveFindFlagsWithName(actionFuncName, command, parentCommand, foundName) + + for _, flag := range flags { + flag.Apply(flagSet) + } + } +} + +func recursiveFindFlagsWithName(actionFuncName string, command cli.Command, parent string, foundName bool) []cli.Flag { + if command.Action != nil { + if actionFuncName == getFuncName(command.Action) && foundName { + return command.Flags + } + } + + for _, subcommand := range command.Subcommands { + if !foundName { + foundName = strings.EqualFold(subcommand.Name, parent) + } + + found := recursiveFindFlagsWithName(actionFuncName, subcommand, parent, foundName) + if found != nil { + return found + } + } + return nil +} + +func getFuncName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} + +func mustJSON[T any](bytes []byte) *T { + typedPayload := new(T) + err := json.Unmarshal(bytes, typedPayload) + if err != nil { + PanicErr(err) + } + return typedPayload +} From 2f1487564a0a73743423b15ac4af6b981173327f Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:10:52 -0700 Subject: [PATCH 02/33] Add streams trigger template --- .../keystone/templates/streams_trigger.toml | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 core/scripts/keystone/templates/streams_trigger.toml diff --git a/core/scripts/keystone/templates/streams_trigger.toml b/core/scripts/keystone/templates/streams_trigger.toml new file mode 100644 index 0000000000..370ed64e73 --- /dev/null +++ b/core/scripts/keystone/templates/streams_trigger.toml @@ -0,0 +1,76 @@ +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | {{ feed_id }} | verifier_proxy {{ verifier_proxy_id }}" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "{{ contract_id }}" +relay = "evm" +pluginType = "mercury" +feedID = "{{ feed_id }}" +transmitterID = "{{ transmitter_id }}" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "{{ chain_id }}" +enableTriggerCapability = true +fromBlock = "{{ from_block }}" + +[pluginConfig] +linkFeedID = "{{ link_feed_id }}" +nativeFeedID = "{{ native_feed_id }}" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" From 0defb7d9939e956e3f99f92f71bdf43e2e7eb34b Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:11:14 -0700 Subject: [PATCH 03/33] Add mock external adapter for v03 mercury --- .../external-adapter/99_external_adapter.go | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 core/scripts/keystone/src/external-adapter/99_external_adapter.go diff --git a/core/scripts/keystone/src/external-adapter/99_external_adapter.go b/core/scripts/keystone/src/external-adapter/99_external_adapter.go new file mode 100644 index 0000000000..4c768ce4cc --- /dev/null +++ b/core/scripts/keystone/src/external-adapter/99_external_adapter.go @@ -0,0 +1,85 @@ +package main + +// Taken from https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/integration_test.go#L1055 +import ( + "fmt" + "math/rand" + "net" + "net/http" + "net/http/httptest" +) + +func main() { + // Simulating MATIC/USD + initialValue := 0.4 + pctBounds := 0.3 + + externalAdapter(initialValue, "4001", pctBounds) + externalAdapter(initialValue, "4002", pctBounds) + externalAdapter(initialValue, "4003", pctBounds) + + select {} +} + +func externalAdapter(initialValue float64, port string, pctBounds float64) *httptest.Server { + // Create a custom listener on the specified port + listener, err := net.Listen("tcp", "localhost:"+port) + if err != nil { + panic(err) + } + + mid := initialValue + // we make step a tenth of the pctBounds to give ample room for the value to move + step := mid * pctBounds / 10 + bid := mid - step + ask := mid + step + // Calculate the floor and ceiling based on percentages of the initial value + ceiling := float64(initialValue) * (1 + pctBounds) + floor := float64(initialValue) * (1 - pctBounds) + + handler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + // [floor <= bid <= mid <= ask <= ceiling] + mid = adjustValue(mid, step, floor, ceiling) + bid = adjustValue(mid, step, floor, mid) + ask = adjustValue(mid, step, mid, ceiling) + + resp := fmt.Sprintf(`{"result": {"bid": %.4f, "mid": %.4f, "ask": %.4f}}`, bid, mid, ask) + + _, herr := res.Write([]byte(resp)) + if herr != nil { + fmt.Printf("failed to write response: %v", herr) + } + }) + + ea := &httptest.Server{ + Listener: listener, + Config: &http.Server{Handler: handler}, + } + ea.Start() + + fmt.Print("Mock external adapter started at ", ea.URL, "\n") + fmt.Printf("Initial value: %.4f, Floor: %.4f, Ceiling: %.4f\n", initialValue, floor, ceiling) + return ea +} + +// adjustValue takes a starting value and randomly shifts it up or down by a step. +// It ensures that the value stays within the specified bounds. +func adjustValue(start, step, floor, ceiling float64) float64 { + // Randomly choose to increase or decrease the value + if rand.Intn(2) == 0 { + step = -step + } + + // Apply the step to the starting value + newValue := start + step + + // Ensure the value is within the bounds + if newValue < floor { + newValue = floor + } else if newValue > ceiling { + newValue = ceiling + } + + return newValue +} From 1b4d298f91e0bffe2a5c73bb079ca471b487369d Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:17:11 -0700 Subject: [PATCH 04/33] First pass of streams trigger provisioning --- .../04_deploy_streams_trigger-sample.sh | 12 + core/scripts/keystone/main.go | 1 + .../src/03_deploy_streams_trigger_cmd.go | 241 +++++++++++ .../src/03_deploy_streams_trigger_cmd_test.go | 44 ++ core/scripts/keystone/src/99_files.go | 1 + .../03_deploy_streams_trigger_cmd_test.snap | 395 ++++++++++++++++++ 6 files changed, 694 insertions(+) create mode 100755 core/scripts/keystone/04_deploy_streams_trigger-sample.sh create mode 100644 core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go create mode 100644 core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go create mode 100755 core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap diff --git a/core/scripts/keystone/04_deploy_streams_trigger-sample.sh b/core/scripts/keystone/04_deploy_streams_trigger-sample.sh new file mode 100755 index 0000000000..5ccb9bcbc5 --- /dev/null +++ b/core/scripts/keystone/04_deploy_streams_trigger-sample.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# This is for arb sepolia, see jobspec on https://cl-df-mercury-arb-sepolia-0.main.stage.cldev.sh/jobs/34/definition +go run main.go \ + deploy-streams-trigger \ + --verifierproxycontractaddress=$VERIFIER_PROXY_CONTRACT_ADDRESS \ + --verifiercontractaddress=$VERIFIER_CONTRACT_ADDRESS \ + --chainid=$CHAIN_ID \ + --fromblock=$FROM_BLOCK \ + --linkfeedid=$LINK_FEED_ID \ + --nativefeedid=$NATIVE_FEED_ID \ + --feedid=$FEED_ID \ + --dryrun=true diff --git a/core/scripts/keystone/main.go b/core/scripts/keystone/main.go index 3486830ca3..6ded2f7121 100644 --- a/core/scripts/keystone/main.go +++ b/core/scripts/keystone/main.go @@ -22,6 +22,7 @@ func main() { src.NewDeployAndInitializeCapabilitiesRegistryCommand(), src.NewDeployWorkflowsCommand(), src.NewDeleteWorkflowsCommand(), + src.NewDeployStreamsTriggerCommand(), } commandsList := func(commands []command) string { diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go new file mode 100644 index 0000000000..9175e6cb41 --- /dev/null +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -0,0 +1,241 @@ +package src + +// This package deploys "offchainreporting2" job specs, which setup the streams trigger +// for the targetted node set +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/integration_test.go#L92 +// for how to setup the mercury portion of the streams trigger +// You can see how all fields are being used here: https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/helpers_test.go#L314 +// https://github.com/smartcontractkit/infra-k8s/blob/be47098adfb605d79b5bab6aa601bcf443a6c48b/projects/chainlink/files/chainlink-clusters/cl-keystone-cap-one/config.yaml#L1 +// Trigger gets added to the registry here: https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/evm.go#L360 +// See integration workflow here: https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/capabilities/integration_tests/workflow.go#L15 +// ^ setup.go provides good insight too +import ( + "encoding/json" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "net/url" + + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" + "github.com/smartcontractkit/chainlink/v2/core/bridges" + + "github.com/smartcontractkit/chainlink/v2/core/store/models" + "github.com/smartcontractkit/chainlink/v2/core/web/presenters" +) + +type deployStreamsTrigger struct{} + +func NewDeployStreamsTriggerCommand() *deployStreamsTrigger { + return &deployStreamsTrigger{} +} + +func (g *deployStreamsTrigger) Name() string { + return "deploy-streams-trigger" +} + +func (g *deployStreamsTrigger) Run(args []string) { + fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) + chainID := fs.Int64("chainid", 11155111, "chain id") + templatesLocation := fs.String("templates", "", "Custom templates location") + feedID := fs.String("feedid", "", "Feed ID") + linkFeedID := fs.String("linkfeedid", "", "Link Feed ID") + nativeFeedID := fs.String("nativefeedid", "", "Native Feed ID") + fromBlock := fs.Int64("fromblock", 0, "From block") + nodeList := fs.String("nodes", "", "Custom node list location") + publicKeys := fs.String("publickeys", "", "Custom public keys json location") + verifierContractAddress := fs.String("verifiercontractaddress", "", "Verifier contract address") + verifierProxyContractAddress := fs.String("verifierproxycontractaddress", "", "Verifier proxy contract address") + dryrun := fs.Bool("dryrun", false, "Dry run") + + err := fs.Parse(args) + if err != nil || chainID == nil || *chainID == 0 || + feedID == nil || *feedID == "" || + linkFeedID == nil || *linkFeedID == "" || + nativeFeedID == nil || *nativeFeedID == "" || + fromBlock == nil || *fromBlock == 0 || + verifierContractAddress == nil || *verifierContractAddress == "" || + verifierProxyContractAddress == nil || *verifierProxyContractAddress == "" { + fs.Usage() + os.Exit(1) + } + + if *publicKeys == "" { + *publicKeys = defaultPublicKeys + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + if *templatesLocation == "" { + *templatesLocation = "templates" + } + + nodes := downloadNodeAPICredentials(*nodeList) + + jobspecs := genStreamsTriggerJobSpecs( + *publicKeys, + *nodeList, + *templatesLocation, + + *feedID, + *linkFeedID, + *nativeFeedID, + + *chainID, + *fromBlock, + + *verifierContractAddress, + *verifierProxyContractAddress, + ) + + // sanity check arr lengths + if len(nodes) != len(jobspecs) { + PanicErr(errors.New("Mismatched node and job spec lengths")) + } + + for i, n := range nodes { + api := newNodeAPI(n) + + specToDeploy := strings.Join(jobspecs[i], "\n") + specFragment := jobspecs[i][0:2] + if *dryrun { + fmt.Println("Dry run, skipping job deployment and bridge setup") + fmt.Printf("Deploying jobspec: %s\n... \n", specToDeploy) + continue + } else { + fmt.Printf("Deploying jobspec: %s\n... \n", specFragment) + } + + _, err := api.withArg(specToDeploy).exec(api.methods.CreateJob) + if err != nil { + fmt.Println("Failed to deploy job spec:", specFragment, "Error:", err) + } + + // hard coded bridges for now + createBridgeIfDoesNotExist(api, "bridge-coinmetrics", "http://localhost:4001") + createBridgeIfDoesNotExist(api, "bridge-tiingo", "http://localhost:4002") + createBridgeIfDoesNotExist(api, "bridge-ncfx", "http://localhost:4003") + + } +} + +func createBridgeIfDoesNotExist(api *nodeAPI, name string, eaURL string) { + if doesBridgeExist(api, name) { + fmt.Println("Bridge", name, "already exists, skipping creation") + return + } + + u, err := url.Parse(eaURL) + url := models.WebURL(*u) + // Confirmations and MinimumContractPayment are not used, so we can leave them as 0 + b := bridges.BridgeTypeRequest{ + Name: bridges.MustParseBridgeName(name), + URL: url, + } + payload, err := json.Marshal(b) + helpers.PanicErr(err) + + resp := api.withArg(string(payload)).mustExec(api.methods.CreateBridge) + resource := mustJSON[presenters.BridgeResource](resp) + fmt.Printf("Created bridge: %s %s\n", resource.Name, resource.URL) +} + +func doesBridgeExist(api *nodeAPI, name string) bool { + resp, err := api.withArg(name).exec(api.methods.ShowBridge) + + if err != nil { + return false + } + + b := mustJSON[presenters.BridgeResource](resp) + fmt.Printf("Found bridge: %s with URL: %s\n", b.Name, b.URL) + return true +} + +func genStreamsTriggerJobSpecs( + pubkeysPath string, + nodeListPath string, + templatesDir string, + + feedID string, + linkFeedID string, + nativeFeedID string, + + chainID int64, + fromBlock int64, + + verifierContractAddress string, + verifierProxyContractAddress string, +) (output [][]string) { + nodes := downloadNodeAPICredentials(nodeListPath) + nca := downloadNodePubKeys(nodeListPath, chainID, pubkeysPath) + lines, err := readLines(filepath.Join(templatesDir, streamsTriggerSpecTemplate)) + if err != nil { + PanicErr(err) + } + + for i := 0; i < len(nodes); i++ { + n := nca[i] + specLines := renderStreamsTriggerJobSpec( + lines, + + feedID, + linkFeedID, + nativeFeedID, + + chainID, + fromBlock, + + verifierContractAddress, + verifierProxyContractAddress, + + n, + ) + output = append(output, specLines) + } + + return output +} + +func renderStreamsTriggerJobSpec( + lines []string, + + feedID string, + linkFeedID string, + nativeFeedID string, + + chainID int64, + fromBlock int64, + + verifierContractAddress string, + verifierProxyContractAddress string, + + node NodeKeys, +) (output []string) { + chainIDStr := strconv.FormatInt(chainID, 10) + fromBlockStr := strconv.FormatInt(fromBlock, 10) + for _, l := range lines { + l = strings.Replace(l, "{{ feed_id }}", feedID, 1) + l = strings.Replace(l, "{{ link_feed_id }}", linkFeedID, 1) + l = strings.Replace(l, "{{ native_feed_id }}", nativeFeedID, 1) + + l = strings.Replace(l, "{{ chain_id }}", chainIDStr, 1) + l = strings.Replace(l, "{{ from_block }}", fromBlockStr, 1) + + // Verifier contract https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/integration_test.go#L111 + l = strings.Replace(l, "{{ contract_id }}", verifierContractAddress, 1) + // Ends up just being part of the name as documentation, it's the proxy to the verifier contract + l = strings.Replace(l, "{{ verifier_proxy_id }}", verifierProxyContractAddress, 1) + + // TransmitterID is the CSA key of the node since it's offchain + // https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/helpers_test.go#L219 + // https://github.com/smartcontractkit/chainlink-common/blob/9ee1e8cc8b9774c8f3eb92a722af5269469f46f4/pkg/types/mercury/types.go#L39 + l = strings.Replace(l, "{{ transmitter_id }}", node.CSAPublicKey, 1) + output = append(output, l) + } + return +} diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go new file mode 100644 index 0000000000..ea5ed744d2 --- /dev/null +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go @@ -0,0 +1,44 @@ +package src + +import ( + "strings" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" +) + +func TestGenStreamsTriggerJobSpecs(t *testing.T) { + pubkeysPath := "./testdata/PublicKeys.json" + nodeListPath := "./testdata/NodeList.txt" + templatesDir := "../templates" + + feedID := "feed123" + linkFeedID := "linkfeed123" + nativeFeedID := "nativefeed123" + + chainID := int64(123456) + fromBlock := int64(10) + + verifierContractAddress := "verifier_contract_address" + verifierProxyContractAddress := "verifier_proxy_contract_address" + + output := genStreamsTriggerJobSpecs( + pubkeysPath, + nodeListPath, + templatesDir, + feedID, + linkFeedID, + nativeFeedID, + chainID, + fromBlock, + verifierContractAddress, + verifierProxyContractAddress, + ) + prettyOutputs := []string{} + for _, o := range output { + prettyOutputs = append(prettyOutputs, strings.Join(o, "\n")) + } + + testOutput := strings.Join(prettyOutputs, "\n\n-------------------------------------------------\n\n") + snaps.MatchSnapshot(t, testOutput) +} diff --git a/core/scripts/keystone/src/99_files.go b/core/scripts/keystone/src/99_files.go index 08ba12e419..f1837d2379 100644 --- a/core/scripts/keystone/src/99_files.go +++ b/core/scripts/keystone/src/99_files.go @@ -16,6 +16,7 @@ const ( defaultNodeList = ".cache/NodeList.txt" deployedContractsJSON = "deployed_contracts.json" bootstrapSpecTemplate = "bootstrap.toml" + streamsTriggerSpecTemplate = "streams_trigger.toml" cribOverrideTemplate = "crib-overrides.yaml" oracleSpecTemplate = "oracle.toml" ) diff --git a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap new file mode 100755 index 0000000000..40a5062b54 --- /dev/null +++ b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap @@ -0,0 +1,395 @@ + +[TestGenStreamsTriggerJobSpecs - 1] +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "verifier_contract_address" +relay = "evm" +pluginType = "mercury" +feedID = "feed123" +transmitterID = "csa_dbae6965bad0b0fa95ecc34a602eee1c0c570ddc29b56502e400d18574b8c3df" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "123456" +enableTriggerCapability = true +fromBlock = "10" + +[pluginConfig] +linkFeedID = "linkfeed123" +nativeFeedID = "nativefeed123" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" + +------------------------------------------------- + +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "verifier_contract_address" +relay = "evm" +pluginType = "mercury" +feedID = "feed123" +transmitterID = "csa_c5cc655a9c19b69626519c4a72c44a94a3675daeba9c16cc23e010a7a6dac1be" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "123456" +enableTriggerCapability = true +fromBlock = "10" + +[pluginConfig] +linkFeedID = "linkfeed123" +nativeFeedID = "nativefeed123" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" + +------------------------------------------------- + +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "verifier_contract_address" +relay = "evm" +pluginType = "mercury" +feedID = "feed123" +transmitterID = "csa_7407fc90c70895c0fb2bdf385e2e4918364bec1f7a74bad7fdf696bffafbcab8" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "123456" +enableTriggerCapability = true +fromBlock = "10" + +[pluginConfig] +linkFeedID = "linkfeed123" +nativeFeedID = "nativefeed123" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" + +------------------------------------------------- + +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "verifier_contract_address" +relay = "evm" +pluginType = "mercury" +feedID = "feed123" +transmitterID = "csa_ef55caf17eefc2a9d547b5a3978d396bd237c73af99cd849a4758701122e3cba" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "123456" +enableTriggerCapability = true +fromBlock = "10" + +[pluginConfig] +linkFeedID = "linkfeed123" +nativeFeedID = "nativefeed123" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" + +------------------------------------------------- + +name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" +type = "offchainreporting2" +schemaVersion = 1 +forwardingAllowed = false +maxTaskDuration = "0s" +contractID = "verifier_contract_address" +relay = "evm" +pluginType = "mercury" +feedID = "feed123" +transmitterID = "csa_1b874ac2d54b966cec5a8358678ca6f030261aabf3372ce9dbea2d4eb9cdab3d" +observationSource = """ +// data source 1 +// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices +// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 +ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds1_benchmark [type=jsonparse path="data,mid"]; +ds1_bid [type=jsonparse path="data,bid"]; +ds1_ask [type=jsonparse path="data,ask"]; + +ds1_benchmark_multiply [type=multiply times=1000000000000000000]; +ds1_bid_multiply [type=multiply times=1000000000000000000]; +ds1_ask_multiply [type=multiply times=1000000000000000000]; +// data source 2 +ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds2_benchmark [type=jsonparse path="data,mid"]; +ds2_bid [type=jsonparse path="data,bid"]; +ds2_ask [type=jsonparse path="data,ask"]; + +ds2_benchmark_multiply [type=multiply times=1000000000000000000]; +ds2_bid_multiply [type=multiply times=1000000000000000000]; +ds2_ask_multiply [type=multiply times=1000000000000000000]; +// data source 3 +ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; +ds3_benchmark [type=jsonparse path="data,mid"]; +ds3_bid [type=jsonparse path="data,bid"]; +ds3_ask [type=jsonparse path="data,ask"]; + +ds3_benchmark_multiply [type=multiply times=1000000000000000000]; +ds3_bid_multiply [type=multiply times=1000000000000000000]; +ds3_ask_multiply [type=multiply times=1000000000000000000]; +// benchmark +ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; +ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; +ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; +// The index is what determines how fields get assigned in a mercury report +// benchmark is always index 0 +// bid is always index 1 +// ask is always index 2 +// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info +benchmark_price [type=median allowedFaults=2 index=0]; + +// bid +ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; +ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; +ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; +bid_price [type=median allowedFaults=2 index=1]; + +// ask +ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; +ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; +ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; +ask_price [type=median allowedFaults=2 index=2]; +""" + +[relayConfig] +chainID = "123456" +enableTriggerCapability = true +fromBlock = "10" + +[pluginConfig] +linkFeedID = "linkfeed123" +nativeFeedID = "nativefeed123" +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +# We don't need to specify a mercury server URL here since we're using this as a trigger instead +serverURL = "wss://unknown" +--- From 4b033245554dccdf5d35a5c420b741d20841dd43 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:29:01 -0700 Subject: [PATCH 05/33] WIP: Add capabilities registry provisioner script --- ..._provision_capabilities_registry-sample.sh | 6 + core/scripts/keystone/main.go | 1 + .../keystone/src/01_deploy_contracts_cmd.go | 19 +- ...deploy_initialize_capabilities_registry.go | 74 +-- .../src/06_provision_capabilities_registry.go | 124 +++++ .../keystone/src/88_capabilities_registry.go | 456 ++++++++++++++++++ 6 files changed, 606 insertions(+), 74 deletions(-) create mode 100644 core/scripts/keystone/06_provision_capabilities_registry-sample.sh create mode 100644 core/scripts/keystone/src/06_provision_capabilities_registry.go create mode 100644 core/scripts/keystone/src/88_capabilities_registry.go diff --git a/core/scripts/keystone/06_provision_capabilities_registry-sample.sh b/core/scripts/keystone/06_provision_capabilities_registry-sample.sh new file mode 100644 index 0000000000..ede7cccfdc --- /dev/null +++ b/core/scripts/keystone/06_provision_capabilities_registry-sample.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +go run main.go provision-capabilites-registry \ + --chainid=11155111 \ + --ethurl=$ETH_URL \ + --accountkey=$ACCOUNT_KEY diff --git a/core/scripts/keystone/main.go b/core/scripts/keystone/main.go index 6ded2f7121..2d56494a32 100644 --- a/core/scripts/keystone/main.go +++ b/core/scripts/keystone/main.go @@ -23,6 +23,7 @@ func main() { src.NewDeployWorkflowsCommand(), src.NewDeleteWorkflowsCommand(), src.NewDeployStreamsTriggerCommand(), + src.NewProvisionCapabilitesRegistryCommand(), } commandsList := func(commands []command) string { diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index b304973795..06f076b6f7 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -17,8 +17,9 @@ import ( ) type deployedContracts struct { - OCRContract common.Address `json:"ocrContract"` - ForwarderContract common.Address `json:"forwarderContract"` + OCRContract common.Address `json:"ocrContract"` + ForwarderContract common.Address `json:"forwarderContract"` + CapabilityRegsitry common.Address `json:"capabilityRegistry"` // The block number of the transaction that set the config on the OCR3 contract. We use this to replay blocks from this point on // when we load the OCR3 job specs on the nodes. SetConfigTxBlock uint64 `json:"setConfigTxBlock"` @@ -133,11 +134,7 @@ func deploy( OCRContract: ocrContract.Address(), ForwarderContract: forwarderContract.Address(), } - jsonBytes, err := json.Marshal(contracts) - PanicErr(err) - - err = os.WriteFile(DeployedContractsFilePath(artefacts), jsonBytes, 0600) - PanicErr(err) + WriteDeployedContracts(contracts, artefacts) setOCR3Config(env, ocrConfig, artefacts) @@ -178,9 +175,13 @@ func setOCR3Config( // Write blocknumber of the transaction to the deployed contracts file loadedContracts.SetConfigTxBlock = receipt.BlockNumber.Uint64() - jsonBytes, err := json.Marshal(loadedContracts) + WriteDeployedContracts(loadedContracts, artefacts) +} + +func WriteDeployedContracts(contracts deployedContracts, artefactsDir string) { + jsonBytes, err := json.Marshal(contracts) PanicErr(err) - err = os.WriteFile(DeployedContractsFilePath(artefacts), jsonBytes, 0600) + err = os.WriteFile(DeployedContractsFilePath(artefactsDir), jsonBytes, 0600) PanicErr(err) } diff --git a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go index 3352267d14..434b24b710 100644 --- a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go +++ b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go @@ -2,20 +2,16 @@ package src import ( "context" - "encoding/hex" "flag" "fmt" "log" "os" - "strings" "time" "github.com/ethereum/go-ethereum/common" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" - ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" - capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/values" @@ -31,7 +27,7 @@ type peer struct { } var ( - workflowDonPeers = []peer{ + hardcodedWorkflowDonPeers = []peer{ { PeerID: "12D3KooWBCF1XT5Wi8FzfgNCqRL76Swv8TRU3TiD4QiJm8NMNX7N", Signer: "0x9639dCc7D0ca4468B5f684ef89F12F0B365c9F6d", @@ -61,7 +57,7 @@ var ( Signer: "0x7Fa21F6f716CFaF8f249564D72Ce727253186C89", }, } - triggerDonPeers = []peer{ + hardCodedTriggerDonPeers = []peer{ { PeerID: "12D3KooWBaiTbbRwwt2fbNifiL7Ew9tn3vds9AJE3Nf3eaVBX36m", Signer: "0x9CcE7293a4Cc2621b61193135A95928735e4795F", @@ -91,7 +87,7 @@ var ( Signer: "0x91d9b0062265514f012Eb8fABA59372fD9520f56", }, } - targetDonPeers = []peer{ + hardcodedTargetDonPeers = []peer{ { PeerID: "12D3KooWJrthXtnPHw7xyHFAxo6NxifYTvc8igKYaA6wRRRqtsMb", Signer: "0x3F82750353Ea7a051ec9bA011BC628284f9a5327", @@ -121,58 +117,6 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Name() string { return "deploy-and-initialize-capabilities-registry" } -func peerIDToB(peerID string) ([32]byte, error) { - var peerIDB ragetypes.PeerID - err := peerIDB.UnmarshalText([]byte(peerID)) - if err != nil { - return [32]byte{}, err - } - - return peerIDB, nil -} - -func peers(ps []peer) ([][32]byte, error) { - out := [][32]byte{} - for _, p := range ps { - b, err := peerIDToB(p.PeerID) - if err != nil { - return nil, err - } - - out = append(out, b) - } - - return out, nil -} - -func peerToNode(nopID uint32, p peer) (kcr.CapabilitiesRegistryNodeParams, error) { - peerIDB, err := peerIDToB(p.PeerID) - if err != nil { - return kcr.CapabilitiesRegistryNodeParams{}, fmt.Errorf("failed to convert peerID: %w", err) - } - - sig := strings.TrimPrefix(p.Signer, "0x") - signerB, err := hex.DecodeString(sig) - if err != nil { - return kcr.CapabilitiesRegistryNodeParams{}, fmt.Errorf("failed to convert signer: %w", err) - } - - var sigb [32]byte - copy(sigb[:], signerB) - - return kcr.CapabilitiesRegistryNodeParams{ - NodeOperatorId: nopID, - P2pId: peerIDB, - Signer: sigb, - }, nil -} - -func newCapabilityConfig() *capabilitiespb.CapabilityConfig { - return &capabilitiespb.CapabilityConfig{ - DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), - } -} - // Run expects the following environment variables to be set: // // 1. Deploys the CapabilitiesRegistry contract @@ -276,7 +220,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { nopID := recLog.NodeOperatorId nodes := []kcr.CapabilitiesRegistryNodeParams{} - for _, wfPeer := range workflowDonPeers { + for _, wfPeer := range hardcodedWorkflowDonPeers { n, innerErr := peerToNode(nopID, wfPeer) if innerErr != nil { panic(innerErr) @@ -286,7 +230,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { nodes = append(nodes, n) } - for _, triggerPeer := range triggerDonPeers { + for _, triggerPeer := range hardCodedTriggerDonPeers { n, innerErr := peerToNode(nopID, triggerPeer) if innerErr != nil { panic(innerErr) @@ -296,7 +240,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { nodes = append(nodes, n) } - for _, targetPeer := range targetDonPeers { + for _, targetPeer := range hardcodedTargetDonPeers { n, innerErr := peerToNode(nopID, targetPeer) if innerErr != nil { panic(innerErr) @@ -314,7 +258,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { helpers.ConfirmTXMined(ctx, env.Ec, tx, env.ChainID) // workflow DON - ps, err := peers(workflowDonPeers) + ps, err := peers(hardcodedWorkflowDonPeers) if err != nil { panic(err) } @@ -337,7 +281,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { } // trigger DON - ps, err = peers(triggerDonPeers) + ps, err = peers(hardCodedTriggerDonPeers) if err != nil { panic(err) } @@ -369,7 +313,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { } // target DON - ps, err = peers(targetDonPeers) + ps, err = peers(hardcodedTargetDonPeers) if err != nil { panic(err) } diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go new file mode 100644 index 0000000000..333a9434a3 --- /dev/null +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -0,0 +1,124 @@ +package src + +import ( + "context" + "flag" + "fmt" + "os" + + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +type provisionCR struct{} + +func NewProvisionCapabilitesRegistryCommand() *provisionCR { + return &provisionCR{} +} + +func (c *provisionCR) Name() string { + return "provision-capabilities-registry" +} + +func (c *provisionCR) Run(args []string) { + ctx := context.Background() + + fs := flag.NewFlagSet(c.Name(), flag.ExitOnError) + // create flags for all of the env vars then set the env vars to normalize the interface + // this is a bit of a hack but it's the easiest way to make this work + ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") + chainID := fs.Int64("chainid", 11155111, "chain ID of the Ethereum network to deploy to") + accountKey := fs.String("accountkey", "", "private key of the account to deploy from") + publicKeys := fs.String("publickeys", "", "Custom public keys json location") + nodeList := fs.String("nodes", "", "Custom node list location") + artefactsDir := fs.String("artefacts", "", "Custom artefacts directory location") + + err := fs.Parse(args) + if err != nil || + *ethUrl == "" || ethUrl == nil || + *chainID == 0 || chainID == nil || + *accountKey == "" || accountKey == nil { + fs.Usage() + os.Exit(1) + } + + if *artefactsDir == "" { + *artefactsDir = defaultArtefactsDir + } + if *publicKeys == "" { + *publicKeys = defaultPublicKeys + } + if *nodeList == "" { + *nodeList = defaultNodeList + } + + os.Setenv("ETH_URL", *ethUrl) + os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) + os.Setenv("ACCOUNT_KEY", *accountKey) + + env := helpers.SetupEnv(false) + + reg := getOrDeployCapabilitiesRegistry(ctx, *artefactsDir, env) + + // For now, trigger, target, and workflow DONs are the same node sets, and same don instance + // + // CHECKME: I *think* we can make different DON instances across the same nodes if we provision mulitiple OCR3 jobs, each other a different OCR2 key bundle so we get distinct signers yet the same PeerIDs, which makes sense since the nodes are the same + workflowDON := loadDON( + *publicKeys, + *chainID, + *nodeList, + ) + crProvisioner := NewCapabilityRegistryProvisioner(reg) + // We're using the default capability set for now + capSet := NewCapabilitySet() + crProvisioner.AddCapabilities(ctx, capSet) + + nodeOperator := NewNodeOperator(env.Owner.From, "MY_NODE_OPERATOR", workflowDON) + crProvisioner.AddNodeOperator(ctx, nodeOperator) + + // Note that both of these calls are simplified versions of the actual calls + // + // See the method documentation for more details + crProvisioner.AddNodes(ctx, nodeOperator, capSet) + crProvisioner.AddDON(ctx, nodeOperator, capSet, true, true) +} + +func loadDON(publicKeys string, chainID int64, nodeList string) []peer { + nca := downloadNodePubKeys(nodeList, chainID, publicKeys) + workflowDON := []peer{} + for _, n := range nca { + + p := peer{ + PeerID: n.P2PPeerID, + Signer: n.OCR2OnchainPublicKey, + } + workflowDON = append(workflowDON, p) + } + return workflowDON +} + +func getOrDeployCapabilitiesRegistry(ctx context.Context, artefactsDir string, env helpers.Environment) *kcr.CapabilitiesRegistry { + contracts, err := LoadDeployedContracts(artefactsDir) + if err != nil { + panic(err) + } + + if contracts.CapabilityRegsitry.String() == "0x" { + _, tx, capabilitiesRegistry, innerErr := kcr.DeployCapabilitiesRegistry(env.Owner, env.Ec) + if innerErr != nil { + panic(innerErr) + } + + helpers.ConfirmContractDeployed(ctx, env.Ec, tx, env.ChainID) + contracts.CapabilityRegsitry = capabilitiesRegistry.Address() + WriteDeployedContracts(contracts, artefactsDir) + return capabilitiesRegistry + } else { + capabilitiesRegistry, innerErr := kcr.NewCapabilitiesRegistry(contracts.CapabilityRegsitry, env.Ec) + if innerErr != nil { + panic(innerErr) + } + + return capabilitiesRegistry + } +} diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go new file mode 100644 index 0000000000..bf9082008b --- /dev/null +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -0,0 +1,456 @@ +package src + +import ( + "context" + "fmt" + "log" + "time" + + "strings" + + "encoding/hex" + + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" + + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-common/pkg/values" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + gethCommon "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +type CapabilityRegistryProvisioner struct { + reg *kcr.CapabilitiesRegistry + env helpers.Environment +} + +func NewCapabilityRegistryProvisioner(reg *kcr.CapabilitiesRegistry) *CapabilityRegistryProvisioner { + return &CapabilityRegistryProvisioner{reg: reg} +} + +// AddCapabilities takes a capability set and provisions it in the registry. +func (c *CapabilityRegistryProvisioner) AddCapabilities(ctx context.Context, capSet CapabilitySet) { + tx, err := c.reg.AddCapabilities(c.env.Owner, capSet.Capabilities()) + if err != nil { + log.Printf("failed to call AddCapabilities: %s", err) + } + + helpers.ConfirmTXMined(ctx, c.env.Ec, tx, c.env.ChainID) +} + +// AddNodeOperator takes a node operator and provisions it in the registry. +// +// A node operator is a group of nodes that are all controlled by the same entity. The admin address is the +// address that controls the node operator. +// +// The name is a human-readable name for the node operator. +// +// The node operator is then added to the registry, and the registry will issue an ID for the node operator. +// The ID is then used when adding nodes to the registry such that the registry knows which nodes belong to which +// node operator. +func (c *CapabilityRegistryProvisioner) AddNodeOperator(ctx context.Context, nop NodeOperator) { + nop.BindToRegistry(c.reg) + tx, err := c.reg.AddNodeOperators(c.env.Owner, []kcr.CapabilitiesRegistryNodeOperator{ + { + Admin: nop.Admin, + Name: nop.Name, + }, + }) + if err != nil { + log.Printf("failed to AddNodeOperators: %s", err) + } + + receipt := helpers.ConfirmTXMined(ctx, c.env.Ec, tx, c.env.ChainID) + nop.SetCapabilityRegistryIssuedID(receipt) +} + +// AddNodes takes a node operators nodes, along with a capability set, then configures the registry such that +// each node is assigned the same capability set. The registry will then know that each node supports each of the +// capabilities in the set. +// +// This is a simplified version of the actual implementation, which is more flexible. The actual implementation +// allows for the ability to add different capability sets to different nodes, _and_ lets you add nodes from different +// node operators to the same capability set. This is not yet implemented here. +// +// Note that the registry must already have the capability set added via `AddCapabilities`, you cannot +// add capabilites that the registry is not yet aware of. +// +// Note that in terms of the provisioning process, this is not the last step. A capability is only active once +// there is a DON servicing yet. This is done via `AddDON`. +func (c *CapabilityRegistryProvisioner) AddNodes(ctx context.Context, nop NodeOperator, capSet CapabilitySet) { + params := []kcr.CapabilitiesRegistryNodeParams{} + for _, peer := range nop.DON { + node, innerErr := peerToNode(nop.id, peer) + if innerErr != nil { + panic(innerErr) + } + + // Technically we could be more flexible here, + // where we can have different capset assignment for each node + node.HashedCapabilityIds = capSet.CapabilityIDs() + + params = append(params, node) + } + + tx, err := c.reg.AddNodes(c.env.Owner, params) + if err != nil { + log.Printf("failed to AddNodes: %s", err) + } + helpers.ConfirmTXMined(ctx, c.env.Ec, tx, c.env.ChainID) +} + +// AddDON takes a node operator, a capability set, and provisions a DON with the given capabilities. +// +// A DON is a group of nodes that all support the same capability set. This set can be a subset of the +// capabilities that the nodes support. In other words, each node within the node set can support +// a different, possibly overlapping, set of capabilities, but a DON is a subgroup of those nodes that all support +// the same set of capabilities. +// +// A node can belong to multiple DONs, but it must belong to one and only one workflow DON. +// +// A DON can be a capability DON or a workflow DON, or both. +// +// When you want to add solely a workflow DON, you should set `acceptsWorkflows` to true and +// `isPublic` to false. +// This means that the DON can service workflow requests and will not service external capability requests. +// +// If you want to add solely a capability DON, you should set `acceptsWorkflows` to false and `isPublic` to true. This means that the DON +// will service external capability requests and reject workflow requests. +// +// If you want to add a DON that services both capabilities and workflows, you should set both `acceptsWorkflows` and `isPublic` to true. +// +// Another important distinction is that DON can comprise of nodes from different node operators, but for now, we're keeping it simple and restricting it to a single node operator. We also hard code F to 1. +func (c *CapabilityRegistryProvisioner) AddDON(ctx context.Context, nop NodeOperator, capSet CapabilitySet, isPublic bool, acceptsWorkflows bool) { + configs := capSet.Configs(c.reg) + + // Note: Technically we could be more flexible here, + // where we can have multiple DONs with different capability configurations + // and have a non-hardcoded number for F + tx, err := c.reg.AddDON(c.env.Owner, nop.MustGetPeerIDs(), configs, isPublic, acceptsWorkflows, 1) + if err != nil { + log.Printf("failed to AddDON: %s", err) + } + helpers.ConfirmTXMined(ctx, c.env.Ec, tx, c.env.ChainID) +} + +/* + * + * Capabilities + * + * + */ +const ( // Taken from https://github.com/smartcontractkit/chainlink/blob/29117850e9be1be1993dbf8f21cf13cbb6af9d24/core/capabilities/integration_tests/keystone_contracts_setup.go#L43 + CapabilityTypeTrigger = 0 + CapabilityTypeAction = 1 + CapabilityTypeConsensus = 2 + CapabilityTypeTarget = 3 +) + +type CapabillityProvisioner interface { + Config() kcr.CapabilitiesRegistryCapabilityConfiguration + Capability() kcr.CapabilitiesRegistryCapability + BindToRegistry(reg *kcr.CapabilitiesRegistry) + GetHashedCID() [32]byte +} + +type baseCapability struct { + registry *kcr.CapabilitiesRegistry + capability kcr.CapabilitiesRegistryCapability +} + +func (b *baseCapability) BindToRegistry(reg *kcr.CapabilitiesRegistry) { + b.registry = reg +} + +func (b *baseCapability) GetHashedCID() [32]byte { + if b.registry == nil { + panic(fmt.Errorf("registry not bound to capability, cannot get hashed capability ID")) + } + + return mustHashCapabilityID(b.registry, b.capability) +} + +func (b *baseCapability) config(config *capabilitiespb.CapabilityConfig) kcr.CapabilitiesRegistryCapabilityConfiguration { + configBytes, err := proto.Marshal(config) + if err != nil { + panic(err) + } + + return kcr.CapabilitiesRegistryCapabilityConfiguration{ + Config: configBytes, + CapabilityId: b.GetHashedCID(), + } +} + +func (b *baseCapability) Capability() kcr.CapabilitiesRegistryCapability { + return b.capability +} + +type ConsensusCapability struct { + baseCapability +} +var _ CapabillityProvisioner = &ConsensusCapability{} + +func (c *ConsensusCapability) Config() kcr.CapabilitiesRegistryCapabilityConfiguration { + // Note that this is hard-coded for now, we'll want to support more flexible configurations in the future + // for configuring consensus once it has more configuration options + config := &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), + } + + return c.config(config) +} + +// NewOCR3V1ConsensusCapability returns a new ConsensusCapability for OCR3 +func NewOCR3V1ConsensusCapability() *ConsensusCapability { + return &ConsensusCapability{ + baseCapability{ + capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: "offchain_reporting", + Version: "1.0.0", + CapabilityType: CapabilityTypeConsensus, + }, + }, + } +} + +type TargetCapability struct { + baseCapability +} +var _ CapabillityProvisioner = &TargetCapability{} + +func (t *TargetCapability) Config() kcr.CapabilitiesRegistryCapabilityConfiguration { + // Note that this is hard-coded for now, we'll want to support more flexible configurations in the future + // for configuring the target. This configuration is also specific to the write target + config := &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), + RemoteConfig: &capabilitiespb.CapabilityConfig_RemoteTargetConfig{ + RemoteTargetConfig: &capabilitiespb.RemoteTargetConfig{ + RequestHashExcludedAttributes: []string{"signed_report.Signatures"}, + }, + }, + } + + return t.config(config) +} + +func NewEthereumSepoliaWriteTargetV1Capability() *TargetCapability { + return &TargetCapability{ + baseCapability{ + capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: "write_ethereum-testnet_sepolia", + Version: "1.0.0", + CapabilityType: CapabilityTypeTarget, + }, + }, + } +} + +type TriggerCapability struct { + baseCapability +} + +var _ CapabillityProvisioner = &TriggerCapability{} + +func (t *TriggerCapability) Config() kcr.CapabilitiesRegistryCapabilityConfiguration { + // Note that this is hard-coded for now, we'll want to support more flexible configurations in the future + // for configuring the trigger. This configuration is also possibly specific to the streams trigger. + config := &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), + RemoteConfig: &capabilitiespb.CapabilityConfig_RemoteTriggerConfig{ + RemoteTriggerConfig: &capabilitiespb.RemoteTriggerConfig{ + RegistrationRefresh: durationpb.New(20 * time.Second), + RegistrationExpiry: durationpb.New(60 * time.Second), + // We've hardcoded F + 1 here + MinResponsesToAggregate: uint32(1) + 1, + }, + }, + } + + return t.config(config) +} + +func NewStreamsTriggerV1Capability() *TriggerCapability { + return &TriggerCapability{ + baseCapability{ + capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: "streams-trigger", + Version: "1.0.0", + CapabilityType: CapabilityTypeTrigger, + }, + }, + } +} + +func mustHashCapabilityID(reg *kcr.CapabilitiesRegistry, capability kcr.CapabilitiesRegistryCapability) [32]byte { + hashedCapabilityID, err := reg.GetHashedCapabilityId(&bind.CallOpts{}, capability.LabelledName, capability.Version) + if err != nil { + panic(err) + } + return hashedCapabilityID +} + +/* + * + * Capabililty Sets + * + * + */ +type CapabilitySet []CapabillityProvisioner + +func NewCapabilitySet(capabilities ...CapabillityProvisioner) CapabilitySet { + if len(capabilities) == 0 { + log.Println("No capabilities provided, using default capabillity set") + return []CapabillityProvisioner{ + NewStreamsTriggerV1Capability(), + NewEthereumSepoliaWriteTargetV1Capability(), + NewOCR3V1ConsensusCapability(), + } + } + + return capabilities +} + +func (c *CapabilitySet) Capabilities() []kcr.CapabilitiesRegistryCapability { + var definitions []kcr.CapabilitiesRegistryCapability + for _, cap := range *c { + definitions = append(definitions, cap.Capability()) + } + + return definitions +} + +func (c *CapabilitySet) CapabilityIDs() [][32]byte { + var ids [][32]byte + for _, cap := range *c { + ids = append(ids, cap.GetHashedCID()) + } + + return ids +} + +func (c *CapabilitySet) Configs(reg *kcr.CapabilitiesRegistry) []kcr.CapabilitiesRegistryCapabilityConfiguration { + var configs []kcr.CapabilitiesRegistryCapabilityConfiguration + for _, cap := range *c { + cap.BindToRegistry(reg) + configs = append(configs, cap.Config()) + } + + return configs +} + +/* + * + * Node Operator + * + * + */ + +type NodeOperator struct { + Admin gethCommon.Address + Name string + // This is a really simplified mapping + // We dont handle multichain, multi don, etc + // Take a look at https://github.com/smartcontractkit/chainlink/pull/14334/files#diff-9cd09c4e7efeae20108eea3eeeb1119bb923eecce51e98d9066ea0cee19af09c for a more complete version + // but we'll integrate with them in the future + DON []peer + + reg *kcr.CapabilitiesRegistry + // This ID is generated by the registry when the NodeOperator is added + id uint32 +} + +func NewNodeOperator(admin gethCommon.Address, name string, don []peer) NodeOperator { + return NodeOperator{ + Admin: admin, + Name: name, + DON: don, + } +} + +func (n *NodeOperator) BindToRegistry(reg *kcr.CapabilitiesRegistry) { + n.reg = reg +} + +func (n *NodeOperator) SetCapabilityRegistryIssuedID(receipt *gethTypes.Receipt) uint32 { + if n.reg == nil { + panic(fmt.Errorf("registry not bound to node operator, cannot set ID")) + } + // We'll need more complex handling for multiple node operators + // since we'll need to handle log ordering + recLog, err := n.reg.ParseNodeOperatorAdded(*receipt.Logs[0]) + if err != nil { + panic(err) + } + + n.id = recLog.NodeOperatorId + return n.id +} + +func (n *NodeOperator) MustGetPeerIDs() [][32]byte { + ps, err := peers(n.DON) + if err != nil { + panic(err) + } + + return ps +} + +func peerIDToB(peerID string) ([32]byte, error) { + var peerIDB ragetypes.PeerID + err := peerIDB.UnmarshalText([]byte(peerID)) + if err != nil { + return [32]byte{}, err + } + + return peerIDB, nil +} + +func peers(ps []peer) ([][32]byte, error) { + out := [][32]byte{} + for _, p := range ps { + b, err := peerIDToB(p.PeerID) + if err != nil { + return nil, err + } + + out = append(out, b) + } + + return out, nil +} + +func peerToNode(nopID uint32, p peer) (kcr.CapabilitiesRegistryNodeParams, error) { + peerIDB, err := peerIDToB(p.PeerID) + if err != nil { + return kcr.CapabilitiesRegistryNodeParams{}, fmt.Errorf("failed to convert peerID: %w", err) + } + + sig := strings.TrimPrefix(p.Signer, "0x") + signerB, err := hex.DecodeString(sig) + if err != nil { + return kcr.CapabilitiesRegistryNodeParams{}, fmt.Errorf("failed to convert signer: %w", err) + } + + var sigb [32]byte + copy(sigb[:], signerB) + + return kcr.CapabilitiesRegistryNodeParams{ + NodeOperatorId: nopID, + P2pId: peerIDB, + Signer: sigb, + }, nil +} + +func newCapabilityConfig() *capabilitiespb.CapabilityConfig { + return &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), + } +} From 7154202c6f68d3480a9715587f6c94271b742a7e Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:04:27 -0700 Subject: [PATCH 06/33] Update nix flake --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 77dddea406..a8ec244d4a 100644 --- a/flake.lock +++ b/flake.lock @@ -56,11 +56,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1725103162, - "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", + "lastModified": 1725983898, + "narHash": "sha256-4b3A9zPpxAxLnkF9MawJNHDtOOl6ruL0r6Og1TEDGCE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", + "rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43", "type": "github" }, "original": { From 7e0602cf31cf5d5c65d9bc6600d4d8edcf325ba7 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:28:13 -0700 Subject: [PATCH 07/33] Fixup provisioning scripts --- .../src/06_provision_capabilities_registry.go | 2 -- core/scripts/keystone/src/99_app.go | 16 +++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index 333a9434a3..2ab5303115 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -61,8 +61,6 @@ func (c *provisionCR) Run(args []string) { reg := getOrDeployCapabilitiesRegistry(ctx, *artefactsDir, env) // For now, trigger, target, and workflow DONs are the same node sets, and same don instance - // - // CHECKME: I *think* we can make different DON instances across the same nodes if we provision mulitiple OCR3 jobs, each other a different OCR2 key bundle so we get distinct signers yet the same PeerIDs, which makes sense since the nodes are the same workflowDON := loadDON( *publicKeys, *chainID, diff --git a/core/scripts/keystone/src/99_app.go b/core/scripts/keystone/src/99_app.go index 9a8fbad9e9..e736c02165 100644 --- a/core/scripts/keystone/src/99_app.go +++ b/core/scripts/keystone/src/99_app.go @@ -6,13 +6,12 @@ import ( "errors" "flag" "fmt" + "github.com/urfave/cli" "io" "reflect" "runtime" "strings" - "github.com/urfave/cli" - "github.com/smartcontractkit/chainlink/v2/core/cmd" helpers "github.com/smartcontractkit/chainlink/core/scripts/common" @@ -33,6 +32,7 @@ func newApp(n *node, writer io.Writer) (*clcmd.Shell, *cli.App) { app := clcmd.NewApp(client) fs := flag.NewFlagSet("blah", flag.ContinueOnError) fs.String("remote-node-url", n.url.String(), "") + fs.Bool("insecure-skip-verify", true, "") helpers.PanicErr(app.Before(cli.NewContext(nil, fs, nil))) // overwrite renderer since it's set to stdout after Before() is called client.Renderer = clcmd.RendererJSON{Writer: writer} @@ -58,16 +58,18 @@ func newNodeAPI(n *node) *nodeAPI { fs: flag.NewFlagSet("test", flag.ContinueOnError), } - api.withFlags(api.methods.RemoteLogin, - func(fs *flag.FlagSet) { - fs.Set("bypass-version-check", fmt.Sprint(true)) - }).mustExec() + fmt.Println("Logging in:", n.url) + loginFs := flag.NewFlagSet("test", flag.ContinueOnError) + loginFs.Bool("bypass-version-check", true, "") + loginCtx := cli.NewContext(app, loginFs, nil) + err := methods.RemoteLogin(loginCtx) + helpers.PanicErr(err) + output.Reset() return api } func (c *nodeAPI) withArg(arg string) *nodeAPI { - err := c.fs.Parse([]string{arg}) helpers.PanicErr(err) From 29980a48ddaa702d247aef22f9d6026d70a7d7d6 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:44:51 -0700 Subject: [PATCH 08/33] Change default chainid to be 1337 --- core/scripts/keystone/01_deploy_contracts-sample.sh | 2 +- core/scripts/keystone/02_deploy_jobspecs-sample.sh | 2 +- core/scripts/keystone/03_gen_crib-sample.sh | 2 +- ...ploy_and_initialize_capabilities_registry-sample.sh | 2 +- core/scripts/keystone/src/01_deploy_contracts_cmd.go | 2 +- core/scripts/keystone/src/02_deploy_jobspecs_cmd.go | 2 +- .../keystone/src/03_gen_crib_cluster_overrides_cmd.go | 2 +- .../src/03_gen_crib_cluster_overrides_cmd_test.go | 2 +- .../src/05_deploy_initialize_capabilities_registry.go | 2 +- core/scripts/keystone/src/88_gen_jobspecs_test.go | 2 +- core/scripts/keystone/src/88_gen_ocr3_config_test.go | 2 +- .../03_gen_crib_cluster_overrides_cmd_test.snap | 10 +++++----- .../src/__snapshots__/88_gen_jobspecs_test.snap | 10 +++++----- core/scripts/keystone/templates/crib-overrides.yaml | 10 +++++----- 14 files changed, 26 insertions(+), 26 deletions(-) diff --git a/core/scripts/keystone/01_deploy_contracts-sample.sh b/core/scripts/keystone/01_deploy_contracts-sample.sh index 89e77f4556..6853b48920 100755 --- a/core/scripts/keystone/01_deploy_contracts-sample.sh +++ b/core/scripts/keystone/01_deploy_contracts-sample.sh @@ -3,7 +3,7 @@ go run main.go \ deploy-contracts \ --ocrfile=ocr_config.json \ - --chainid=11155111 \ + --chainid=1337 \ --ethurl=ETH_URL \ --accountkey=ACCOUNT_KEY \ --onlysetconfig=false \ diff --git a/core/scripts/keystone/02_deploy_jobspecs-sample.sh b/core/scripts/keystone/02_deploy_jobspecs-sample.sh index e99d54e0d3..212c0d820f 100755 --- a/core/scripts/keystone/02_deploy_jobspecs-sample.sh +++ b/core/scripts/keystone/02_deploy_jobspecs-sample.sh @@ -2,6 +2,6 @@ go run main.go \ deploy-jobspecs \ - --chainid=11155111 \ + --chainid=1337 \ --p2pport=6690 \ --onlyreplay=false diff --git a/core/scripts/keystone/03_gen_crib-sample.sh b/core/scripts/keystone/03_gen_crib-sample.sh index 9193ef4f75..2271f2dbe2 100755 --- a/core/scripts/keystone/03_gen_crib-sample.sh +++ b/core/scripts/keystone/03_gen_crib-sample.sh @@ -2,5 +2,5 @@ go run main.go \ generate-crib \ - --chainid=11155111 \ + --chainid=1337 \ --outpath=/tmp diff --git a/core/scripts/keystone/05_deploy_and_initialize_capabilities_registry-sample.sh b/core/scripts/keystone/05_deploy_and_initialize_capabilities_registry-sample.sh index 21c764be0e..0fec03e7b4 100755 --- a/core/scripts/keystone/05_deploy_and_initialize_capabilities_registry-sample.sh +++ b/core/scripts/keystone/05_deploy_and_initialize_capabilities_registry-sample.sh @@ -2,7 +2,7 @@ go run main.go \ deploy-and-initialize-capabilities-registry \ - --chainid=11155111 \ + --chainid=1337 \ --ethurl=$ETH_URL \ --accountkey=$ACCOUNT_KEY \ --craddress=$CR_ADDRESS \ // 0x0d36aAC2Fd9d6d1C1F59251be6A2B337af27C52B diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index 06f076b6f7..259a218da0 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -48,7 +48,7 @@ func (g *deployContracts) Run(args []string) { // create flags for all of the env vars then set the env vars to normalize the interface // this is a bit of a hack but it's the easiest way to make this work ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") - chainID := fs.Int64("chainid", 11155111, "chain ID of the Ethereum network to deploy to") + chainID := fs.Int64("chainid", 1337, "chain ID of the Ethereum network to deploy to") accountKey := fs.String("accountkey", "", "private key of the account to deploy from") skipFunding := fs.Bool("skipfunding", false, "skip funding the transmitters") onlySetConfig := fs.Bool("onlysetconfig", false, "set the config on the OCR3 contract without deploying the contracts or funding transmitters") diff --git a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go index abc8b64160..76298dc193 100644 --- a/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go +++ b/core/scripts/keystone/src/02_deploy_jobspecs_cmd.go @@ -21,7 +21,7 @@ func (g *deployJobSpecs) Name() string { func (g *deployJobSpecs) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) - chainID := fs.Int64("chainid", 11155111, "chain id") + chainID := fs.Int64("chainid", 1337, "chain id") p2pPort := fs.Int64("p2pport", 6690, "p2p port") onlyReplay := fs.Bool("onlyreplay", false, "only replay the block from the OCR3 contract setConfig transaction") templatesLocation := fs.String("templates", "", "Custom templates location") diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go index 6b98951459..b731d66cb9 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go @@ -21,7 +21,7 @@ func (g *generateCribClusterOverrides) Name() string { func (g *generateCribClusterOverrides) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) - chainID := fs.Int64("chainid", 11155111, "chain id") + chainID := fs.Int64("chainid", 1337, "chain id") outputPath := fs.String("outpath", "../crib", "the path to output the generated overrides") publicKeys := fs.String("publickeys", "", "Custom public keys json location") nodeList := fs.String("nodes", "", "Custom node list location") diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go index 53d43c2342..ae42028261 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go @@ -8,7 +8,7 @@ import ( ) func TestGenerateCribConfig(t *testing.T) { - chainID := int64(11155111) + chainID := int64(1337) templatesDir := "../templates" forwarderAddress := "0x1234567890abcdef" publicKeysPath := "./testdata/PublicKeys.json" diff --git a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go index 434b24b710..cd3bf5c968 100644 --- a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go +++ b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go @@ -128,7 +128,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { // create flags for all of the env vars then set the env vars to normalize the interface // this is a bit of a hack but it's the easiest way to make this work ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") - chainID := fs.Int64("chainid", 11155111, "chain ID of the Ethereum network to deploy to") + chainID := fs.Int64("chainid", 1337, "chain ID of the Ethereum network to deploy to") accountKey := fs.String("accountkey", "", "private key of the account to deploy from") capabilityRegistryAddress := fs.String("craddress", "", "address of the capability registry") diff --git a/core/scripts/keystone/src/88_gen_jobspecs_test.go b/core/scripts/keystone/src/88_gen_jobspecs_test.go index 7af11646c4..8e021bb500 100644 --- a/core/scripts/keystone/src/88_gen_jobspecs_test.go +++ b/core/scripts/keystone/src/88_gen_jobspecs_test.go @@ -28,7 +28,7 @@ func (d *donHostSpec) ToString() string { func TestGenSpecs(t *testing.T) { pubkeysPath := "./testdata/PublicKeys.json" nodeListPath := "./testdata/NodeList.txt" - chainID := int64(11155111) + chainID := int64(1337) p2pPort := int64(6690) contractAddress := "0xB29934624cAe3765E33115A9530a13f5aEC7fa8A" diff --git a/core/scripts/keystone/src/88_gen_ocr3_config_test.go b/core/scripts/keystone/src/88_gen_ocr3_config_test.go index 10cdc07b20..da22397c1c 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config_test.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config_test.go @@ -10,7 +10,7 @@ import ( func TestGenerateOCR3Config(t *testing.T) { // Generate OCR3 config - config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 11155111, "./testdata/PublicKeys.json") + config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 1337, "./testdata/PublicKeys.json") matchOffchainConfig := match.Custom("OffchainConfig", func(s any) (any, error) { // coerce the value to a string diff --git a/core/scripts/keystone/src/__snapshots__/03_gen_crib_cluster_overrides_cmd_test.snap b/core/scripts/keystone/src/__snapshots__/03_gen_crib_cluster_overrides_cmd_test.snap index 08b79a9f4f..0da6693fb2 100755 --- a/core/scripts/keystone/src/__snapshots__/03_gen_crib_cluster_overrides_cmd_test.snap +++ b/core/scripts/keystone/src/__snapshots__/03_gen_crib_cluster_overrides_cmd_test.snap @@ -8,12 +8,12 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' node2: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '0x8B60FDcc9CAC8ea476b31d17011CB204471431d9' ForwarderAddress = '0x1234567890abcdef' @@ -21,7 +21,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '0x6620F516F29979B214e2451498a057FDd3a0A85d' ForwarderAddress = '0x1234567890abcdef' @@ -29,7 +29,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '0xFeB61E22FCf4F9740c9D96b05199F195bd61A7c2' ForwarderAddress = '0x1234567890abcdef' @@ -37,7 +37,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '0x882Fd04D78A7e7D386Dd5b550f19479E5494B0B2' ForwarderAddress = '0x1234567890abcdef' diff --git a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap index b21c1549db..d172d57456 100755 --- a/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap +++ b/core/scripts/keystone/src/__snapshots__/88_gen_jobspecs_test.snap @@ -9,7 +9,7 @@ contractID = "0xB29934624cAe3765E33115A9530a13f5aEC7fa8A" relay = "evm" [relayConfig] -chainID = "11155111" +chainID = "1337" providerType = "ocr3-capability" Oracles: @@ -28,7 +28,7 @@ pluginType = "plugin" transmitterID = "0x8B60FDcc9CAC8ea476b31d17011CB204471431d9" [relayConfig] -chainID = "11155111" +chainID = "1337" [pluginConfig] command = "chainlink-ocr3-capability" @@ -58,7 +58,7 @@ pluginType = "plugin" transmitterID = "0x6620F516F29979B214e2451498a057FDd3a0A85d" [relayConfig] -chainID = "11155111" +chainID = "1337" [pluginConfig] command = "chainlink-ocr3-capability" @@ -88,7 +88,7 @@ pluginType = "plugin" transmitterID = "0xFeB61E22FCf4F9740c9D96b05199F195bd61A7c2" [relayConfig] -chainID = "11155111" +chainID = "1337" [pluginConfig] command = "chainlink-ocr3-capability" @@ -118,7 +118,7 @@ pluginType = "plugin" transmitterID = "0x882Fd04D78A7e7D386Dd5b550f19479E5494B0B2" [relayConfig] -chainID = "11155111" +chainID = "1337" [pluginConfig] command = "chainlink-ocr3-capability" diff --git a/core/scripts/keystone/templates/crib-overrides.yaml b/core/scripts/keystone/templates/crib-overrides.yaml index baeaa5fa1d..758433bbc9 100644 --- a/core/scripts/keystone/templates/crib-overrides.yaml +++ b/core/scripts/keystone/templates/crib-overrides.yaml @@ -6,12 +6,12 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' node2: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '{{ node_2_address }}' ForwarderAddress = '{{ forwarder_address }}' @@ -19,7 +19,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '{{ node_3_address }}' ForwarderAddress = '{{ forwarder_address }}' @@ -27,7 +27,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '{{ node_4_address }}' ForwarderAddress = '{{ forwarder_address }}' @@ -35,7 +35,7 @@ helm: image: ${runtime.images.app} overridesToml: |- [[EVM]] - ChainID = '11155111' + ChainID = '1337' [EVM.Workflow] FromAddress = '{{ node_5_address }}' ForwarderAddress = '{{ forwarder_address }}' From fc07b49d77d317f8ce365b74dcaa540419fc1fa7 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:29:47 -0700 Subject: [PATCH 09/33] Add nil check for balances --- core/scripts/keystone/src/99_fetch_keys.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scripts/keystone/src/99_fetch_keys.go b/core/scripts/keystone/src/99_fetch_keys.go index b115a7bb94..6b705c1ed5 100644 --- a/core/scripts/keystone/src/99_fetch_keys.go +++ b/core/scripts/keystone/src/99_fetch_keys.go @@ -203,7 +203,7 @@ func findEvmOCR2Bundle(ocr2Bundles []ocr2Bundle) int { func findFirstGoodEthKeyAddress(chainID int64, ethKeys []presenters.ETHKeyResource) (string, error) { for _, ethKey := range ethKeys { if ethKey.EVMChainID.Equal(ubig.NewI(chainID)) && !ethKey.Disabled { - if ethKey.EthBalance.IsZero() { + if ethKey.EthBalance == nil || ethKey.EthBalance.IsZero() { fmt.Println("WARN: selected ETH address has zero balance", ethKey.Address) } return ethKey.Address, nil From 2785ec71856d4ebc42c75c37f3b57e5ac4a1238d Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:30:15 -0700 Subject: [PATCH 10/33] Add ability to skip tls verification for local dev --- core/scripts/common/helpers.go | 16 ++++++++++++---- .../keystone/src/01_deploy_contracts_cmd.go | 2 +- ...05_deploy_initialize_capabilities_registry.go | 1 + .../src/06_provision_capabilities_registry.go | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/scripts/common/helpers.go b/core/scripts/common/helpers.go index 57c8c15e40..97ca2dd492 100644 --- a/core/scripts/common/helpers.go +++ b/core/scripts/common/helpers.go @@ -3,10 +3,12 @@ package common import ( "context" "crypto/ecdsa" + "crypto/tls" "encoding/hex" "flag" "fmt" "math/big" + "net/http" "os" "strconv" "strings" @@ -69,11 +71,17 @@ func SetupEnv(overrideNonce bool) Environment { panic("need account key") } - ec, err := ethclient.Dial(ethURL) - PanicErr(err) - - jsonRPCClient, err := rpc.Dial(ethURL) + insecureSkipVerify := os.Getenv("INSECURE_SKIP_VERIFY") == "true" + tr := &http.Transport{ + // User enables this at their own risk! + // #nosec G402 + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}, + } + httpClient := &http.Client{Transport: tr} + rpcConfig := rpc.WithHTTPClient(httpClient) + jsonRPCClient, err := rpc.DialOptions(context.Background(), ethURL, rpcConfig) PanicErr(err) + ec := ethclient.NewClient(jsonRPCClient) chainID, err := strconv.ParseInt(chainIDEnv, 10, 64) PanicErr(err) diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index 259a218da0..b61c691023 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -81,7 +81,7 @@ func (g *deployContracts) Run(args []string) { os.Setenv("ETH_URL", *ethUrl) os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) os.Setenv("ACCOUNT_KEY", *accountKey) - + os.Setenv("INSECURE_SKIP_VERIFY", "true") deploy(*nodeList, *publicKeys, *ocrConfigFile, *skipFunding, *dryRun, *onlySetConfig, *artefactsDir) } diff --git a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go index cd3bf5c968..d039edcc4d 100644 --- a/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go +++ b/core/scripts/keystone/src/05_deploy_initialize_capabilities_registry.go @@ -144,6 +144,7 @@ func (c *deployAndInitializeCapabilitiesRegistryCommand) Run(args []string) { os.Setenv("ETH_URL", *ethUrl) os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) os.Setenv("ACCOUNT_KEY", *accountKey) + os.Setenv("INSECURE_SKIP_VERIFY", "true") env := helpers.SetupEnv(false) diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index 2ab5303115..3e725b6169 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -55,6 +55,7 @@ func (c *provisionCR) Run(args []string) { os.Setenv("ETH_URL", *ethUrl) os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) os.Setenv("ACCOUNT_KEY", *accountKey) + os.Setenv("INSECURE_SKIP_VERIFY", "true") env := helpers.SetupEnv(false) From 1338c9cfd0cf0c9723ca9b3cc2220d40c9258119 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:30:53 -0700 Subject: [PATCH 11/33] Gently fail on not loading contracts for ocr job deletion --- core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go b/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go index 136691962d..ac7668ae57 100644 --- a/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go +++ b/core/scripts/keystone/src/04_delete_ocr3_jobs_cmd.go @@ -61,7 +61,10 @@ func (g *deleteJobs) Run(args []string) { } deployedContracts, err := LoadDeployedContracts(*artefactsDir) - helpers.PanicErr(err) + if err != nil { + fmt.Println("Error loading deployed contracts, skipping:", err) + return + } nodes := downloadNodeAPICredentials(*nodeList) for _, node := range nodes { From 0881e8ab2915b181dc6a10cad080e89d989be772 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:03:31 -0700 Subject: [PATCH 12/33] fixup! Change default chainid to be 1337 --- core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go | 2 +- core/scripts/keystone/src/06_provision_capabilities_registry.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index 9175e6cb41..e0b0783057 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -40,7 +40,7 @@ func (g *deployStreamsTrigger) Name() string { func (g *deployStreamsTrigger) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) - chainID := fs.Int64("chainid", 11155111, "chain id") + chainID := fs.Int64("chainid", 1337, "chain id") templatesLocation := fs.String("templates", "", "Custom templates location") feedID := fs.String("feedid", "", "Feed ID") linkFeedID := fs.String("linkfeedid", "", "Link Feed ID") diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index 3e725b6169..fc64b87c99 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -27,7 +27,7 @@ func (c *provisionCR) Run(args []string) { // create flags for all of the env vars then set the env vars to normalize the interface // this is a bit of a hack but it's the easiest way to make this work ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") - chainID := fs.Int64("chainid", 11155111, "chain ID of the Ethereum network to deploy to") + chainID := fs.Int64("chainid", 1337, "chain ID of the Ethereum network to deploy to") accountKey := fs.String("accountkey", "", "private key of the account to deploy from") publicKeys := fs.String("publickeys", "", "Custom public keys json location") nodeList := fs.String("nodes", "", "Custom node list location") From 0cb9a2e016c6d4efe8f7513ad842217768c56bbc Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:35:32 -0700 Subject: [PATCH 13/33] Formatting --- core/scripts/keystone/src/06_provision_capabilities_registry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index fc64b87c99..eef45e1f4f 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -35,8 +35,8 @@ func (c *provisionCR) Run(args []string) { err := fs.Parse(args) if err != nil || - *ethUrl == "" || ethUrl == nil || *chainID == 0 || chainID == nil || + *ethUrl == "" || ethUrl == nil || *accountKey == "" || accountKey == nil { fs.Usage() os.Exit(1) From 85ddd1a22ace08545d63b8f96f61c8e6ffa53a7f Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:36:11 -0700 Subject: [PATCH 14/33] Change ocr file flag default --- core/scripts/keystone/src/01_deploy_contracts_cmd.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index b61c691023..f3345f6b6d 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -44,7 +44,7 @@ func (g *deployContracts) Name() string { // 5. Funds the transmitters func (g *deployContracts) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ExitOnError) - ocrConfigFile := fs.String("ocrfile", "config_example.json", "path to OCR config file") + ocrConfigFile := fs.String("ocrfile", "ocr_config.json", "path to OCR config file") // create flags for all of the env vars then set the env vars to normalize the interface // this is a bit of a hack but it's the easiest way to make this work ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") @@ -106,6 +106,7 @@ func deploy( configFile, env.ChainID, publicKeys, + OnChainTransmitter, ) if dryRun { From 49b6a4d51511025463c80737425a7a67a0eaafc2 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:38:47 -0700 Subject: [PATCH 15/33] Allow for multiple OCR2KB selection in key fetching --- core/scripts/keystone/src/99_fetch_keys.go | 196 +++++++++++---------- 1 file changed, 103 insertions(+), 93 deletions(-) diff --git a/core/scripts/keystone/src/99_fetch_keys.go b/core/scripts/keystone/src/99_fetch_keys.go index 6b705c1ed5..72149155c6 100644 --- a/core/scripts/keystone/src/99_fetch_keys.go +++ b/core/scripts/keystone/src/99_fetch_keys.go @@ -1,33 +1,30 @@ package src import ( - "bytes" "encoding/json" "errors" - "flag" "fmt" "os" "sort" "strings" - "github.com/urfave/cli" - helpers "github.com/smartcontractkit/chainlink/core/scripts/common" ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/cmd" "github.com/smartcontractkit/chainlink/v2/core/web/presenters" ) -func downloadNodePubKeys(nodeList string, chainID int64, pubKeysPath string) []NodeKeys { - // Check if file exists already, and if so, return the keys +func downloadAllNodeKeys(nodeList string, chainID int64, pubKeysPath string) []AllNodeKeys { if _, err := os.Stat(pubKeysPath); err == nil { fmt.Println("Loading existing public keys at:", pubKeysPath) - return mustParseJSON[[]NodeKeys](pubKeysPath) + allKeys := mustParseJSON[[]AllNodeKeys](pubKeysPath) + return allKeys } nodes := downloadNodeAPICredentials(nodeList) - nodesKeys := mustFetchNodesKeys(chainID, nodes) + allKeys := mustFetchAllNodeKeys(chainID, nodes) - marshalledNodeKeys, err := json.MarshalIndent(nodesKeys, "", " ") + marshalledNodeKeys, err := json.MarshalIndent(allKeys, "", " ") if err != nil { panic(err) } @@ -37,7 +34,18 @@ func downloadNodePubKeys(nodeList string, chainID int64, pubKeysPath string) []N } fmt.Println("Keystone OCR2 public keys have been saved to:", pubKeysPath) - return nodesKeys + return allKeys +} + +func downloadNodePubKeys(nodeList string, chainID int64, pubKeysPath string, index ...int) []NodeKeys { + keys := []NodeKeys{} + allKeys := downloadAllNodeKeys(nodeList, chainID, pubKeysPath) + + for _, k := range allKeys { + keys = append(keys, k.toNodeKeys(index...)) + } + + return keys } // downloadNodeAPICredentials downloads the node API credentials, or loads them from disk if they already exist @@ -87,101 +95,93 @@ func clNodesWithCredsToNodes(clNodesWithCreds []CLNodeCredentials) []*node { return nodes } -type ocr2Bundle struct { - ID string `json:"id"` - ChainType string `json:"chainType"` - OnchainPublicKey string `json:"onchainPublicKey"` - OffchainPublicKey string `json:"offchainPublicKey"` - ConfigPublicKey string `json:"configPublicKey"` +func trimmedOCR2KB(ocr2Bndl cmd.OCR2KeyBundlePresenter) OCR2KBTrimmed { + return OCR2KBTrimmed{ + OCR2BundleID: ocr2Bndl.ID, + OCR2ConfigPublicKey: strings.TrimPrefix(ocr2Bndl.ConfigPublicKey, "ocr2cfg_evm_"), + OCR2OnchainPublicKey: strings.TrimPrefix(ocr2Bndl.OnchainPublicKey, "ocr2on_evm_"), + OCR2OffchainPublicKey: strings.TrimPrefix(ocr2Bndl.OffChainPublicKey, "ocr2off_evm_"), + } } -func mustFetchNodesKeys(chainID int64, nodes []*node) (nca []NodeKeys) { - for _, n := range nodes { - output := &bytes.Buffer{} - client, app := newApp(n, output) - - fmt.Println("Logging in:", n.url) - loginFs := flag.NewFlagSet("test", flag.ContinueOnError) - loginFs.Bool("bypass-version-check", true, "") - loginCtx := cli.NewContext(app, loginFs, nil) - err := client.RemoteLogin(loginCtx) - helpers.PanicErr(err) - output.Reset() +type AllNodeKeys struct { + EthAddress string + P2PPeerID string // p2p_ + OCR2KBs []OCR2KBTrimmed + CSAPublicKey string +} - err = client.ListETHKeys(&cli.Context{ - App: app, - }) - helpers.PanicErr(err) - var ethKeys []presenters.ETHKeyResource - helpers.PanicErr(json.Unmarshal(output.Bytes(), ðKeys)) - ethAddress, err := findFirstGoodEthKeyAddress(chainID, ethKeys) - helpers.PanicErr(err) - output.Reset() +func (a AllNodeKeys) toNodeKeys(index ...int) NodeKeys { + i := 0 + if len(index) > 0 { + i = index[0] + } + if i >= len(a.OCR2KBs) { + panic("index out of range") + } + + return NodeKeys{ + OCR2KBTrimmed: OCR2KBTrimmed{ + OCR2BundleID: a.OCR2KBs[i].OCR2BundleID, + OCR2ConfigPublicKey: a.OCR2KBs[i].OCR2ConfigPublicKey, + OCR2OnchainPublicKey: a.OCR2KBs[i].OCR2OnchainPublicKey, + OCR2OffchainPublicKey: a.OCR2KBs[i].OCR2OffchainPublicKey, + }, + EthAddress: a.EthAddress, + P2PPeerID: a.P2PPeerID, + CSAPublicKey: a.CSAPublicKey, + } +} + +func mustFetchAllNodeKeys(chainId int64, nodes []*node) []AllNodeKeys { + allNodeKeys := []AllNodeKeys{} - err = client.ListP2PKeys(&cli.Context{ - App: app, - }) + for _, n := range nodes { + api := newNodeAPI(n) + eKey := api.mustExec(api.methods.ListETHKeys) + ethKeys := mustJSON[[]presenters.ETHKeyResource](eKey) + ethAddress, err := findFirstGoodEthKeyAddress(chainId, *ethKeys) helpers.PanicErr(err) - var p2pKeys []presenters.P2PKeyResource - helpers.PanicErr(json.Unmarshal(output.Bytes(), &p2pKeys)) - if len(p2pKeys) != 1 { + + p2pKeys := api.mustExec(api.methods.ListP2PKeys) + p2pKey := mustJSON[[]presenters.P2PKeyResource](p2pKeys) + if len(*p2pKey) != 1 { helpers.PanicErr(errors.New("node must have single p2p key")) } - peerID := strings.TrimPrefix(p2pKeys[0].PeerID, "p2p_") - output.Reset() - - var ocr2Bundles []ocr2Bundle - err = client.ListOCR2KeyBundles(&cli.Context{ - App: app, - }) - helpers.PanicErr(err) - helpers.PanicErr(json.Unmarshal(output.Bytes(), &ocr2Bundles)) - ocr2BundleIndex := findEvmOCR2Bundle(ocr2Bundles) - output.Reset() - if ocr2BundleIndex == -1 { - fmt.Println("WARN: node does not have EVM OCR2 bundle, creating one") - fs := flag.NewFlagSet("test", flag.ContinueOnError) - err = fs.Parse([]string{"evm"}) - helpers.PanicErr(err) - ocr2CreateBundleCtx := cli.NewContext(app, fs, nil) - err = client.CreateOCR2KeyBundle(ocr2CreateBundleCtx) - helpers.PanicErr(err) - output.Reset() - - err = client.ListOCR2KeyBundles(&cli.Context{ - App: app, - }) - helpers.PanicErr(err) - helpers.PanicErr(json.Unmarshal(output.Bytes(), &ocr2Bundles)) - ocr2BundleIndex = findEvmOCR2Bundle(ocr2Bundles) - output.Reset() + peerID := strings.TrimPrefix((*p2pKey)[0].PeerID, "p2p_") + + bundles := api.mustExec(api.methods.ListOCR2KeyBundles) + ocr2Bundles := mustJSON[cmd.OCR2KeyBundlePresenters](bundles) + + ocr2EvmBundles := getTrimmedEVMOCR2KBs(*ocr2Bundles) + bundleLen := len(ocr2EvmBundles) + if bundleLen < 2 { + fmt.Printf("WARN: node has %d EVM OCR2 bundles when it should have at least 2, creating bundles...\n", bundleLen) + for i := bundleLen; i < 2; i++ { + cBundle := api.withArg("evm").mustExec(api.methods.CreateOCR2KeyBundle) + fmt.Println("Created OCR2 bundle", string(cBundle)) + createdBundle := mustJSON[cmd.OCR2KeyBundlePresenter](cBundle) + fmt.Printf("Created bundle %s\n", createdBundle.ID) + ocr2EvmBundles = append(ocr2EvmBundles, trimmedOCR2KB(*createdBundle)) + } } - ocr2Bndl := ocr2Bundles[ocr2BundleIndex] - - err = client.ListCSAKeys(&cli.Context{ - App: app, - }) + csaKeys := api.mustExec(api.methods.ListCSAKeys) + csaKeyResources := mustJSON[[]presenters.CSAKeyResource](csaKeys) + csaPubKey, err := findFirstCSAPublicKey(*csaKeyResources) helpers.PanicErr(err) - var csaKeys []presenters.CSAKeyResource - helpers.PanicErr(json.Unmarshal(output.Bytes(), &csaKeys)) - csaPubKey, err := findFirstCSAPublicKey(csaKeys) - helpers.PanicErr(err) - output.Reset() - - nc := NodeKeys{ - EthAddress: ethAddress, - P2PPeerID: peerID, - OCR2BundleID: ocr2Bndl.ID, - OCR2ConfigPublicKey: strings.TrimPrefix(ocr2Bndl.ConfigPublicKey, "ocr2cfg_evm_"), - OCR2OnchainPublicKey: strings.TrimPrefix(ocr2Bndl.OnchainPublicKey, "ocr2on_evm_"), - OCR2OffchainPublicKey: strings.TrimPrefix(ocr2Bndl.OffchainPublicKey, "ocr2off_evm_"), - CSAPublicKey: csaPubKey, + + nodeKeys := AllNodeKeys{ + EthAddress: ethAddress, + P2PPeerID: peerID, + OCR2KBs: ocr2EvmBundles, + CSAPublicKey: strings.TrimPrefix(csaPubKey, "csa_"), } - nca = append(nca, nc) + allNodeKeys = append(allNodeKeys, nodeKeys) } - return + + return allNodeKeys } func findFirstCSAPublicKey(csaKeyResources []presenters.CSAKeyResource) (string, error) { @@ -191,7 +191,7 @@ func findFirstCSAPublicKey(csaKeyResources []presenters.CSAKeyResource) (string, return "", errors.New("did not find any CSA Key Resources") } -func findEvmOCR2Bundle(ocr2Bundles []ocr2Bundle) int { +func findEvmOCR2Bundle(ocr2Bundles cmd.OCR2KeyBundlePresenters) int { for i, b := range ocr2Bundles { if b.ChainType == "evm" { return i @@ -200,6 +200,16 @@ func findEvmOCR2Bundle(ocr2Bundles []ocr2Bundle) int { return -1 } +func getTrimmedEVMOCR2KBs(ocr2Bundles cmd.OCR2KeyBundlePresenters) []OCR2KBTrimmed { + evmBundles := []OCR2KBTrimmed{} + for _, b := range ocr2Bundles { + if b.ChainType == "evm" { + evmBundles = append(evmBundles, trimmedOCR2KB(b)) + } + } + return evmBundles +} + func findFirstGoodEthKeyAddress(chainID int64, ethKeys []presenters.ETHKeyResource) (string, error) { for _, ethKey := range ethKeys { if ethKey.EVMChainID.Equal(ubig.NewI(chainID)) && !ethKey.Disabled { From 3eccbb338ef1def8de8de0ad341d1431031305fb Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:39:20 -0700 Subject: [PATCH 16/33] Support on/offchain transmitter OCR3 config generation --- .../keystone/src/88_gen_ocr3_config.go | 94 +++++++++----- .../keystone/src/88_gen_ocr3_config_test.go | 2 +- .../88_gen_ocr3_config_test.snap | 20 +-- .../keystone/src/testdata/PublicKeys.json | 120 +++++++++++++----- 4 files changed, 158 insertions(+), 78 deletions(-) diff --git a/core/scripts/keystone/src/88_gen_ocr3_config.go b/core/scripts/keystone/src/88_gen_ocr3_config.go index d9280ffe24..aed395acd3 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config.go @@ -47,19 +47,28 @@ type OracleConfigSource struct { MaxFaultyOracles int } -type NodeKeys struct { - EthAddress string - P2PPeerID string // p2p_ +// This is an OCR key bundle with the prefixes on each respective key +// trimmed off +type OCR2KBTrimmed struct { OCR2BundleID string // used only in job spec OCR2OnchainPublicKey string // ocr2on_evm_ OCR2OffchainPublicKey string // ocr2off_evm_ OCR2ConfigPublicKey string // ocr2cfg_evm_ - CSAPublicKey string +} + +type NodeKeys struct { + OCR2KBTrimmed + EthAddress string + P2PPeerID string // p2p_ + CSAPublicKey string } type orc2drOracleConfig struct { - Signers [][]byte - Transmitters []common.Address + Signers [][]byte + // populated when transmitterType == OnChainTransmitter + Transmitters []common.Address + // populated when transmitterType == OffChainTransmitter + OffChainTransmitters [][32]byte F uint8 OnchainConfig []byte OffchainConfigVersion uint64 @@ -98,10 +107,17 @@ func mustReadConfig(fileName string) (output TopLevelConfigSource) { return mustParseJSON[TopLevelConfigSource](fileName) } -func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKeysPath string) orc2drOracleConfig { +type TransmitterType string + +const ( + OnChainTransmitter TransmitterType = "onchain" + OffChainTransmitter TransmitterType = "offchain" +) + +func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKeysPath string, transmitterType TransmitterType, kbIndex ...int) orc2drOracleConfig { topLevelCfg := mustReadConfig(configFile) cfg := topLevelCfg.OracleConfig - nca := downloadNodePubKeys(nodeList, chainID, pubKeysPath) + nca := downloadNodePubKeys(nodeList, chainID, pubKeysPath, kbIndex...) onchainPubKeys := [][]byte{} allPubKeys := map[string]any{} @@ -128,42 +144,33 @@ func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKe offchainPubKeysBytes := []types.OffchainPublicKey{} for _, n := range nca { - pkBytes, err := hex.DecodeString(n.OCR2OffchainPublicKey) - if err != nil { - panic(err) - } - - pkBytesFixed := [ed25519.PublicKeySize]byte{} - nCopied := copy(pkBytesFixed[:], pkBytes) - if nCopied != ed25519.PublicKeySize { - panic("wrong num elements copied from ocr2 offchain public key") - } + pkBytesFixed := strToBytes32(n.OCR2OffchainPublicKey) offchainPubKeysBytes = append(offchainPubKeysBytes, types.OffchainPublicKey(pkBytesFixed)) } configPubKeysBytes := []types.ConfigEncryptionPublicKey{} for _, n := range nca { - pkBytes, err := hex.DecodeString(n.OCR2ConfigPublicKey) - helpers.PanicErr(err) - - pkBytesFixed := [ed25519.PublicKeySize]byte{} - n := copy(pkBytesFixed[:], pkBytes) - if n != ed25519.PublicKeySize { - panic("wrong num elements copied") - } - + pkBytesFixed := strToBytes32(n.OCR2ConfigPublicKey) configPubKeysBytes = append(configPubKeysBytes, types.ConfigEncryptionPublicKey(pkBytesFixed)) } identities := []confighelper.OracleIdentityExtra{} for index := range nca { + var transmitterAccount types.Account + if transmitterType == OnChainTransmitter { + transmitterAccount = types.Account(nca[index].EthAddress) + } + if transmitterType == OffChainTransmitter { + transmitterAccount = types.Account(fmt.Sprintf("%x", nca[index].CSAPublicKey[:])) + } + identities = append(identities, confighelper.OracleIdentityExtra{ OracleIdentity: confighelper.OracleIdentity{ OnchainPublicKey: onchainPubKeys[index][:], OffchainPublicKey: offchainPubKeysBytes[index], PeerID: nca[index].P2PPeerID, - TransmitAccount: types.Account(nca[index].EthAddress), + TransmitAccount: transmitterAccount, }, ConfigEncryptionPublicKey: configPubKeysBytes[index], }) @@ -195,17 +202,40 @@ func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKe configSigners = append(configSigners, signer) } - transmitterAddresses, err := evm.AccountToAddress(transmitters) - PanicErr(err) - config := orc2drOracleConfig{ Signers: configSigners, - Transmitters: transmitterAddresses, F: f, OnchainConfig: onchainConfig, OffchainConfigVersion: offchainConfigVersion, OffchainConfig: offchainConfig, } + if transmitterType == OnChainTransmitter { + transmitterAddresses, err := evm.AccountToAddress(transmitters) + PanicErr(err) + config.Transmitters = transmitterAddresses + } + if transmitterType == OffChainTransmitter { + var offChainTransmitters [][32]byte + for _, n := range nca { + fmt.Println("CSAPublicKey", n.CSAPublicKey) + offChainTransmitters = append(offChainTransmitters, strToBytes32(n.CSAPublicKey)) + } + config.OffChainTransmitters = offChainTransmitters + } + return config } + +func strToBytes32(str string) [32]byte { + pkBytes, err := hex.DecodeString(str) + helpers.PanicErr(err) + + pkBytesFixed := [ed25519.PublicKeySize]byte{} + n := copy(pkBytesFixed[:], pkBytes) + if n != ed25519.PublicKeySize { + fmt.Printf("wrong num elements copied (%s): %d != 32\n", str, n) + panic("wrong num elements copied") + } + return pkBytesFixed +} diff --git a/core/scripts/keystone/src/88_gen_ocr3_config_test.go b/core/scripts/keystone/src/88_gen_ocr3_config_test.go index da22397c1c..19c6bc50a0 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config_test.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config_test.go @@ -10,7 +10,7 @@ import ( func TestGenerateOCR3Config(t *testing.T) { // Generate OCR3 config - config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 1337, "./testdata/PublicKeys.json") + config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 1337, "./testdata/PublicKeys.json", OnChainTransmitter) matchOffchainConfig := match.Custom("OffchainConfig", func(s any) (any, error) { // coerce the value to a string diff --git a/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap b/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap index b9581afb88..8b1a0baecc 100755 --- a/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap +++ b/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap @@ -6,18 +6,18 @@ "OffchainConfigVersion": 30, "OnchainConfig": "0x", "Signers": [ - "011400a2402db8e549f094ea31e1c0edd77623f4ca5b12", - "0114004af19c802b244d1d085492c3946391c965e10519", - "01140061925685d2b80b121537341d063c4e57b2f9323c", - "011400fd97efd53fc20acc098fcd746c04d8d7540d97e0", - "011400a0b67dc5345a71d02b396147ae2cb75dda63cbe9" + "0xbB9dA64fEfbAe323B45Ec98970863279824cf08D", + "0x26F04f488F6383Cd4099b7aEF16c0DC2e553aDDA", + "0xD754a628C5B34D027e4ccd8d90B54b9403A9B5f2", + "0xe366aEd6fe28F47cC2c463Cda40d4BB8573e15D4", + "0x405Aeb4fa9359A7338217bFc4Fdc77C886A6a6Ee" ], "Transmitters": [ - "0xF4e7e516146c8567F8E8be0ED1f1A92798628d35", - "0x8B60FDcc9CAC8ea476b31d17011CB204471431d9", - "0x6620F516F29979B214e2451498a057FDd3a0A85d", - "0xFeB61E22FCf4F9740c9D96b05199F195bd61A7c2", - "0x882Fd04D78A7e7D386Dd5b550f19479E5494B0B2" + "0x9DfB1E962Fc6363087fB60A131aba2b7884dAD97", + "0x367764B5640E691bbeB99Ff14dEc4EA02421fFd9", + "0x4CC725F1b7a23BC42658cce5c40A0ec3C7f3c925", + "0x5323547CdBb450725c59626A1A6C5608C5c1Ea07", + "0x16d9BbC5e927053697771AF7d7a003FE7b60B61E" ] } --- diff --git a/core/scripts/keystone/src/testdata/PublicKeys.json b/core/scripts/keystone/src/testdata/PublicKeys.json index 7ade3d45ad..07f98a242a 100644 --- a/core/scripts/keystone/src/testdata/PublicKeys.json +++ b/core/scripts/keystone/src/testdata/PublicKeys.json @@ -1,47 +1,97 @@ [ { - "EthAddress": "0xF4e7e516146c8567F8E8be0ED1f1A92798628d35", - "P2PPeerID": "12D3KooWNmhKZL1XW4Vv3rNjLXzJ6mqcVerihdijjGYuexPrFUFZ", - "OCR2BundleID": "2f92c96da20fbe39c89e59516e3a7473254523316887394e406527c72071d3db", - "OCR2OnchainPublicKey": "a2402db8e549f094ea31e1c0edd77623f4ca5b12", - "OCR2OffchainPublicKey": "3ca9918cd2787de8f9aff91f220f30a5cc54c394f73e173b12c93368bd7072ad", - "OCR2ConfigPublicKey": "19904debd03994fe9ea411cda7a6b2f01f20a3fe803df0fed67aaf00cc99113f", - "CSAPublicKey": "csa_dbae6965bad0b0fa95ecc34a602eee1c0c570ddc29b56502e400d18574b8c3df" + "EthAddress": "0x9DfB1E962Fc6363087fB60A131aba2b7884dAD97", + "P2PPeerID": "12D3KooWFwZTEkR3dM8smBeWZTzzXPWAwEBQiGUYNo8gqskS2DcL", + "OCR2KBs": [ + { + "OCR2BundleID": "05ff3c1c2cba85f8761362221afbc0c44a8fce52467c9feb59271479b5f4cc79", + "OCR2OnchainPublicKey": "bb9da64fefbae323b45ec98970863279824cf08d", + "OCR2OffchainPublicKey": "d90d1fcec7c7f52766fcf9f78733f5c963003f1098e4db274481120baa661c23", + "OCR2ConfigPublicKey": "7a5350a05a0c1a76f1dfb2777a9cc4fb96fccafc00cd145c88e038e4376aed1e" + }, + { + "OCR2BundleID": "acfa03eed948b1cde5067161ce27a88ce5bc98905128e678cf1987aa41022119", + "OCR2OnchainPublicKey": "6ff800e9d0b0c0087286b833049a166dfe269f49", + "OCR2OffchainPublicKey": "66edb100a254e4ae3f4856bf3c5c9933908cc893c1733e7df0a2553a44d4dc2a", + "OCR2ConfigPublicKey": "b4b3d403748daf54f56e684b30fcecabad3ab59ac4e59c41fbf6ce5b9efae74d" + } + ], + "CSAPublicKey": "53940b5245216eaae8a87fb777902bf898432820bfc3c97778fd7325e0f4d1f3" }, { - "EthAddress": "0x8B60FDcc9CAC8ea476b31d17011CB204471431d9", - "P2PPeerID": "12D3KooWFUjV73ZYkAMhS2cVwte3kXDWD8Ybyx3u9CEDHNoeEhBH", - "OCR2BundleID": "b3df4d8748b67731a1112e8b45a764941974f5590c93672eebbc4f3504dd10ed", - "OCR2OnchainPublicKey": "4af19c802b244d1d085492c3946391c965e10519", - "OCR2OffchainPublicKey": "365b9e1c3c945fc3f51afb25772f0a5a1f1547935a4b5dc89c012f590709fefe", - "OCR2ConfigPublicKey": "15ff12569d11b8ff9f17f8999ea928d03a439f3fb116661cbc4669a0a3192775", - "CSAPublicKey": "csa_c5cc655a9c19b69626519c4a72c44a94a3675daeba9c16cc23e010a7a6dac1be" + "EthAddress": "0x367764B5640E691bbeB99Ff14dEc4EA02421fFd9", + "P2PPeerID": "12D3KooWJWcdKb5BATxkuApEZufpEsXgu3UAG4fPtvuavHe2NHQi", + "OCR2KBs": [ + { + "OCR2BundleID": "2e302a95ada6b2d8b8fffce6ffd0926a129a11fb59bc9088ac0c8f01f43ba355", + "OCR2OnchainPublicKey": "26f04f488f6383cd4099b7aef16c0dc2e553adda", + "OCR2OffchainPublicKey": "d721c5ec7a62e81b9c8f1b7a2fd6d193b135c7cea207d16af74ab71fc92fdbdc", + "OCR2ConfigPublicKey": "c09ea54fa542f02e459b9a749a3306b4b6cc613812f4bfef17412883bcb9d87b" + }, + { + "OCR2BundleID": "f550286c0e74d894e95ec5e55e06fc980420d30cf2c6565b148f7423922f6b4d", + "OCR2OnchainPublicKey": "ca60dc5200e49b345808ce40b68b4d9d480762fb", + "OCR2OffchainPublicKey": "d8f7d6e9f358bbf69e8a26d7e65ddfd98ecc9758dac131a983a5100216e72b9c", + "OCR2ConfigPublicKey": "72ab5516dea0304fcb3a16a2c1a314b0ccd844aba4cdbe2f8d7944924c990405" + } + ], + "CSAPublicKey": "bb4a327ef258e09fd6192443ad007b0a2fdd5d0fb0a42aeb1c05252ba1c71bb7" }, { - "EthAddress": "0x6620F516F29979B214e2451498a057FDd3a0A85d", - "P2PPeerID": "12D3KooWRTtH2WWrztD87Do1kXePSmGjyU4r7mZVWThmqTGgdbUC", - "OCR2BundleID": "38459ae37f29f2c1fde0f25972a973322be8cada82acf43f464756836725be97", - "OCR2OnchainPublicKey": "61925685d2b80b121537341d063c4e57b2f9323c", - "OCR2OffchainPublicKey": "7fe2dbd9f9fb96f7dbbe0410e32d435ad67dae6c91410189fe5664cf3057ef10", - "OCR2ConfigPublicKey": "2f02fd80b362e1c7acf91680fd48c062718233acd595a6ae7cbe434e118e6a4f", - "CSAPublicKey": "csa_7407fc90c70895c0fb2bdf385e2e4918364bec1f7a74bad7fdf696bffafbcab8" + "EthAddress": "0x4CC725F1b7a23BC42658cce5c40A0ec3C7f3c925", + "P2PPeerID": "12D3KooWR1xT9MRh469Fh21CRK1t1xbveqUCnbgG53UFktU1DrRw", + "OCR2KBs": [ + { + "OCR2BundleID": "3cdac381db901a657fe9416724d69efb46473652c6d8192a67d1f8dcefcc96f7", + "OCR2OnchainPublicKey": "d754a628c5b34d027e4ccd8d90b54b9403a9b5f2", + "OCR2OffchainPublicKey": "db6582f9a70f48b886963cea7facbef92ead0de7c0ac0954e7765a526bab454e", + "OCR2ConfigPublicKey": "f53ccfe1fc347988bc7b7151458517ed985283e6e4ae093f6591f015c1d7762a" + }, + { + "OCR2BundleID": "dceccf3e8451d9d43c6d975f106c192b8581554d18c254a6e9539022bced08f9", + "OCR2OnchainPublicKey": "3cc49cd664c836824f1f77283668b7fe139c8090", + "OCR2OffchainPublicKey": "c5daa1235dd866ec912bfd6aceadf7e3ced29443b570ed826d97e4a4ae3b60a2", + "OCR2ConfigPublicKey": "690e03fc2565fbaed75b8b1516f67e8dd97ff9f02962196830f121f571ba7948" + } + ], + "CSAPublicKey": "2861e869bbc7035b2be949d9c8d06189d4af9f962ecb0dbe8a443c7425cfe688" }, { - "EthAddress": "0xFeB61E22FCf4F9740c9D96b05199F195bd61A7c2", - "P2PPeerID": "12D3KooWMTZnZtcVK4EJsjkKsV9qXNoNRSjT62CZi3tKkXGaCsGh", - "OCR2BundleID": "b5dbc4c9da983cddde2e3226b85807eb7beaf818694a22576af4d80f352702ed", - "OCR2OnchainPublicKey": "fd97efd53fc20acc098fcd746c04d8d7540d97e0", - "OCR2OffchainPublicKey": "91b393bb5e6bd6fd9de23845bcd0e0d9b0dd28a1d65d3cfb1fce9f91bd3d8c19", - "OCR2ConfigPublicKey": "09eb53924ff8b33a08b4eae2f3819015314ce6e8864ac4f86e97caafd4181506", - "CSAPublicKey": "csa_ef55caf17eefc2a9d547b5a3978d396bd237c73af99cd849a4758701122e3cba" + "EthAddress": "0x5323547CdBb450725c59626A1A6C5608C5c1Ea07", + "P2PPeerID": "12D3KooWNeovZBgc4kJWGBi8U8C3rsoyQqZECPqwvgb5E7yBBnzu", + "OCR2KBs": [ + { + "OCR2BundleID": "872bc4c78dc3c751ef100afcd60fa81c7f1744a9a10be1e86f6689455132041e", + "OCR2OnchainPublicKey": "e366aed6fe28f47cc2c463cda40d4bb8573e15d4", + "OCR2OffchainPublicKey": "126d991638b02eadfb614ee011970a1e22ac8838012be58f72eca3cd99326ba9", + "OCR2ConfigPublicKey": "168153f44e3be99df292082ff79eaf6d4972e905c9519c3cc719e416fa9a1e63" + }, + { + "OCR2BundleID": "e63799347d9d0a64e9a7f9715239f640f4df785bee9689898965d83911db3ccb", + "OCR2OnchainPublicKey": "fea23a235be5e3160e88a7a463df0f74f4e035fa", + "OCR2OffchainPublicKey": "93e965f15a1913dd11341b1f829220cda4b3d96b4634fac91183276eed890f52", + "OCR2ConfigPublicKey": "c33577067dffc7af126f654b52d6e176cd0b359024dfdf0bc1dbf381af46ae00" + } + ], + "CSAPublicKey": "05a0b0a1d7c58129ade44a28fc982fe43532160d6367829853da003fbecdc456" }, { - "EthAddress": "0x882Fd04D78A7e7D386Dd5b550f19479E5494B0B2", - "P2PPeerID": "12D3KooWRsM9yordRQDhLgbErH8WMMGz1bC1J4hR5gAGvMWu8goN", - "OCR2BundleID": "260d5c1a618cdf5324509d7db95f5a117511864ebb9e1f709e8969339eb225af", - "OCR2OnchainPublicKey": "a0b67dc5345a71d02b396147ae2cb75dda63cbe9", - "OCR2OffchainPublicKey": "4f42ef42e5cc351dbbd79c29ef33af25c0250cac84837c1ff997bc111199d07e", - "OCR2ConfigPublicKey": "3b90249731beb9e4f598371f0b96c3babf47bcc62121ebc9c195e3c33e4fd708", - "CSAPublicKey": "csa_1b874ac2d54b966cec5a8358678ca6f030261aabf3372ce9dbea2d4eb9cdab3d" + "EthAddress": "0x16d9BbC5e927053697771AF7d7a003FE7b60B61E", + "P2PPeerID": "12D3KooWSCGxPeUVjLTYcUxd8A53k8Ww1pRbJDb553Rury3UfRpq", + "OCR2KBs": [ + { + "OCR2BundleID": "0fb531b570483b6a02bf245d8854f60c4831550e965cf7d16a80badbb907f9ee", + "OCR2OnchainPublicKey": "405aeb4fa9359a7338217bfc4fdc77c886a6a6ee", + "OCR2OffchainPublicKey": "9f57204194c84d7fb97b67427282684dddd85014497340296ea9f144ed8a4e6e", + "OCR2ConfigPublicKey": "9e5d20fd7a5378c8c4d8baf454b0516da864e3a0cc3393e7e1cf2e909b0bcb3f" + }, + { + "OCR2BundleID": "93ecd598651424647332fba6557e60887c2bffb187e7a0bdbbc955dc0276e792", + "OCR2OnchainPublicKey": "7fee2c4f130decaf2dd6041d4c874e98554a1079", + "OCR2OffchainPublicKey": "bb56dd698595251f6640b3e3235c0611a1d189cb2c243002d2c6a791957f7185", + "OCR2ConfigPublicKey": "493850ef7b53443f0763b5ad4fefbcdd512123cae465d7d56cb9d7c488ed8525" + } + ], + "CSAPublicKey": "aaad908b939c7dbd4aec24add746ec35d4510efebe2d090fdac69dde39babfec" } ] \ No newline at end of file From c3b76cbbfcf2376c6d20e3ce8840749512b34e79 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:39:38 -0700 Subject: [PATCH 17/33] Properly reset clientmethod on each invocation --- core/scripts/keystone/src/99_app.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/scripts/keystone/src/99_app.go b/core/scripts/keystone/src/99_app.go index e736c02165..afdbbdb97d 100644 --- a/core/scripts/keystone/src/99_app.go +++ b/core/scripts/keystone/src/99_app.go @@ -99,7 +99,10 @@ func (c *nodeAPI) exec(clientMethod ...func(*cli.Context) error) ([]byte, error) c.output.Reset() defer c.output.Reset() - defer func() { c.fs = flag.NewFlagSet("test", flag.ContinueOnError) }() + defer func() { + c.fs = flag.NewFlagSet("test", flag.ContinueOnError) + c.clientMethod = nil + }() if c.clientMethod == nil { c.clientMethod = clientMethod[0] From f5e862376e980a9cc0e20b84738f8faa1aab6487 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:40:09 -0700 Subject: [PATCH 18/33] Add mercury contract deployment feature --- .../src/03_deploy_streams_trigger_cmd.go | 430 ++++++++++++------ .../src/03_deploy_streams_trigger_cmd_test.go | 59 +-- .../03_deploy_streams_trigger_cmd_test.snap | 403 +++------------- 3 files changed, 376 insertions(+), 516 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index e0b0783057..d1ebf842b9 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -10,24 +10,69 @@ package src // See integration workflow here: https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/capabilities/integration_tests/workflow.go#L15 // ^ setup.go provides good insight too import ( + "context" + "encoding/binary" "encoding/json" - "errors" "flag" "fmt" + "math/big" "os" - "path/filepath" - "strconv" - "strings" "net/url" + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum/go-ethereum/core/types" + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" "github.com/smartcontractkit/chainlink/v2/core/bridges" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" + + // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/fee_manager" + // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/reward_manager" + verifierContract "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/verifier" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/verifier_proxy" "github.com/smartcontractkit/chainlink/v2/core/store/models" "github.com/smartcontractkit/chainlink/v2/core/web/presenters" ) +type feed struct { + id [32]byte + name string + + // we create a bridge for each feed + bridgeName string + bridgeUrl string +} + +func v3FeedID(id [32]byte) [32]byte { + + binary.BigEndian.PutUint16(id[:2], 3) + return id +} + +var feeds = []feed{ + { + v3FeedID([32]byte{5: 1}), + "BTC/USD", + "mock-bridge-btc", + "http://localhost:4000", + }, + { + v3FeedID([32]byte{5: 2}), + "LINK/USD", + "mock-bridge-link", + "http://localhost:4001", + }, + { + v3FeedID([32]byte{5: 3}), + "NATIVE/USD", + "mock-bridge-native", + "http://localhost:4002", + }, +} + type deployStreamsTrigger struct{} func NewDeployStreamsTriggerCommand() *deployStreamsTrigger { @@ -41,25 +86,20 @@ func (g *deployStreamsTrigger) Name() string { func (g *deployStreamsTrigger) Run(args []string) { fs := flag.NewFlagSet(g.Name(), flag.ContinueOnError) chainID := fs.Int64("chainid", 1337, "chain id") - templatesLocation := fs.String("templates", "", "Custom templates location") - feedID := fs.String("feedid", "", "Feed ID") - linkFeedID := fs.String("linkfeedid", "", "Link Feed ID") - nativeFeedID := fs.String("nativefeedid", "", "Native Feed ID") - fromBlock := fs.Int64("fromblock", 0, "From block") + ocrConfigFile := fs.String("ocrfile", "ocr_config.json", "path to OCR config file") nodeList := fs.String("nodes", "", "Custom node list location") publicKeys := fs.String("publickeys", "", "Custom public keys json location") - verifierContractAddress := fs.String("verifiercontractaddress", "", "Verifier contract address") - verifierProxyContractAddress := fs.String("verifierproxycontractaddress", "", "Verifier proxy contract address") - dryrun := fs.Bool("dryrun", false, "Dry run") + force := fs.Bool("force", false, "Force deployment") + + ethUrl := fs.String("ethurl", "", "URL of the Ethereum node") + accountKey := fs.String("accountkey", "", "private key of the account to deploy from") err := fs.Parse(args) - if err != nil || chainID == nil || *chainID == 0 || - feedID == nil || *feedID == "" || - linkFeedID == nil || *linkFeedID == "" || - nativeFeedID == nil || *nativeFeedID == "" || - fromBlock == nil || *fromBlock == 0 || - verifierContractAddress == nil || *verifierContractAddress == "" || - verifierProxyContractAddress == nil || *verifierProxyContractAddress == "" { + if err != nil || + *ocrConfigFile == "" || ocrConfigFile == nil || + chainID == nil || *chainID == 0 || + *ethUrl == "" || ethUrl == nil || + *accountKey == "" || accountKey == nil { fs.Usage() os.Exit(1) } @@ -70,60 +110,260 @@ func (g *deployStreamsTrigger) Run(args []string) { if *nodeList == "" { *nodeList = defaultNodeList } - if *templatesLocation == "" { - *templatesLocation = "templates" - } - nodes := downloadNodeAPICredentials(*nodeList) + os.Setenv("ETH_URL", *ethUrl) + os.Setenv("ETH_CHAIN_ID", fmt.Sprintf("%d", *chainID)) + os.Setenv("ACCOUNT_KEY", *accountKey) + os.Setenv("INSECURE_SKIP_VERIFY", "true") - jobspecs := genStreamsTriggerJobSpecs( - *publicKeys, + env := helpers.SetupEnv(false) + + setupMercuryV03( + env, *nodeList, - *templatesLocation, + *ocrConfigFile, + *chainID, + *publicKeys, + *force, + ) +} - *feedID, - *linkFeedID, - *nativeFeedID, +// See /core/services/ocr2/plugins/mercury/integration_test.go +func setupMercuryV03(env helpers.Environment, nodeListPath string, ocrConfigFilePath string, chainId int64, pubKeysPath string, force bool) { + fmt.Printf("Deploying streams trigger for chain %d\n", chainId) + fmt.Printf("Using OCR config file: %s\n", ocrConfigFilePath) + fmt.Printf("Using node list: %s\n", nodeListPath) + fmt.Printf("Using public keys: %s\n", pubKeysPath) + fmt.Printf("Force: %t\n\n", force) + + fmt.Printf("Deploying Mercury V0.3 contracts\n") + _, _, _, verifier := deployMercuryV03Contracts(env) + // the 0th index is for the OCR3 capability + // where the 1st index is for the mercury OCR2 instance + kbIndex := 1 + nca := downloadNodePubKeys(nodeListPath, chainId, pubKeysPath, kbIndex) + nodes := downloadNodeAPICredentials(nodeListPath) - *chainID, - *fromBlock, + fmt.Printf("Generating OCR3 config\n") + ocrConfig := generateOCR3Config(nodeListPath, ocrConfigFilePath, chainId, pubKeysPath, OffChainTransmitter, kbIndex) + + for _, feed := range feeds { + fmt.Println("Configuring feeds...") + fmt.Printf("FeedID: %x\n", feed.id) + fmt.Printf("FeedName: %s\n", feed.name) + fmt.Printf("BridgeName: %s\n", feed.bridgeName) + fmt.Printf("BridgeURL: %s\n", feed.bridgeUrl) + + fmt.Printf("Setting verifier config\n") + verifier.SetConfig(env.Owner, + feed.id, + ocrConfig.Signers, + ocrConfig.OffChainTransmitters, + ocrConfig.F, + ocrConfig.OnchainConfig, + ocrConfig.OffchainConfigVersion, + ocrConfig.OffchainConfig, + nil, + ) - *verifierContractAddress, - *verifierProxyContractAddress, - ) + fmt.Printf("Deploying OCR2 job specs for feed %s\n", feed.name) + deployOCR2JobSpecsForFeed(nca, nodes, verifier, feed, chainId, force) + } +} - // sanity check arr lengths - if len(nodes) != len(jobspecs) { - PanicErr(errors.New("Mismatched node and job spec lengths")) +func deployMercuryV03Contracts(env helpers.Environment) (linkToken *link_token_interface.LinkToken, nativeToken *link_token_interface.LinkToken, verifierProxy *verifier_proxy.VerifierProxy, verifier *verifierContract.Verifier) { + var confirmDeploy = func(tx *types.Transaction, err error) { + helpers.ConfirmContractDeployed(context.Background(), env.Ec, tx, env.ChainID) + PanicErr(err) + } + var confirmTx = func(tx *types.Transaction, err error) { + helpers.ConfirmTXMined(context.Background(), env.Ec, tx, env.ChainID) + PanicErr(err) } - for i, n := range nodes { - api := newNodeAPI(n) + _, tx, linkToken, err := link_token_interface.DeployLinkToken(env.Owner, env.Ec) + confirmDeploy(tx, err) - specToDeploy := strings.Join(jobspecs[i], "\n") - specFragment := jobspecs[i][0:2] - if *dryrun { - fmt.Println("Dry run, skipping job deployment and bridge setup") - fmt.Printf("Deploying jobspec: %s\n... \n", specToDeploy) - continue + // Not sure if we actually need to have tokens + tx, err = linkToken.Transfer(env.Owner, env.Owner.From, big.NewInt(1000)) + confirmTx(tx, err) + + // We reuse the link token for the native token + _, tx, nativeToken, err = link_token_interface.DeployLinkToken(env.Owner, env.Ec) + confirmDeploy(tx, err) + + // Not sure if we actually need to have tokens + tx, err = nativeToken.Transfer(env.Owner, env.Owner.From, big.NewInt(1000)) + confirmTx(tx, err) + + verifierProxyAddr, tx, verifierProxy, err := verifier_proxy.DeployVerifierProxy(env.Owner, env.Ec, common.Address{}) // zero address for access controller disables access control + confirmDeploy(tx, err) + + verifierAddress, tx, verifier, err := verifierContract.DeployVerifier(env.Owner, env.Ec, verifierProxyAddr) + confirmDeploy(tx, err) + + tx, err = verifierProxy.InitializeVerifier(env.Owner, verifierAddress) + confirmTx(tx, err) + // rewardManagerAddr, _, rewardManager, err := reward_manager.DeployRewardManager(env.Owner, env.Ec, linkTokenAddress) + // PanicErr(err) + // feeManagerAddr, _, _, err := fee_manager.DeployFeeManager(env.Owner, env.Ec, linkTokenAddress, nativeTokenAddress, verifierProxyAddr, rewardManagerAddr) + // PanicErr(err) + // _, err = verifierProxy.SetFeeManager(env.Owner, feeManagerAddr) + // PanicErr(err) + // _, err = rewardManager.SetFeeManager(env.Owner, feeManagerAddr) + // PanicErr(err) + + return +} + +func deployOCR2JobSpecsForFeed(nca []NodeKeys, nodes []*node, verifier *verifierContract.Verifier, feed feed, chainId int64, force bool) { + // we assign the first node as the bootstrap node + for i, n := range nca { + // parallel arrays + api := newNodeAPI(nodes[i]) + jobSpecName := "" + jobSpecStr := "" + + createBridgeIfDoesNotExist(api, feed.bridgeName, feed.bridgeUrl) + if i == 0 { + jobSpecName, jobSpecStr = createMercuryV3BootstrapJob( + verifier.Address(), + feed.name, + feed.id, + chainId, + ) } else { - fmt.Printf("Deploying jobspec: %s\n... \n", specFragment) + jobSpecName, jobSpecStr = createMercuryV3Job( + n.OCR2BundleID, + verifier.Address(), + feed.bridgeName, + n.CSAPublicKey, + fmt.Sprintf("feed-%s", feed.name), + feed.id, + feeds[1].id, + feeds[2].id, + chainId, + ) } - _, err := api.withArg(specToDeploy).exec(api.methods.CreateJob) + jobsResp := api.mustExec(api.methods.ListJobs) + jobs := mustJSON[[]JobSpec](jobsResp) + shouldSkip := false + for _, job := range *jobs { + if job.Name == jobSpecName { + if force { + fmt.Printf("Job already exists: %s, replacing..\n", jobSpecName) + api.withArg(job.Id).mustExec(api.methods.DeleteJob) + fmt.Printf("Deleted job: %s\n", jobSpecName) + } else { + fmt.Printf("Job already exists: %s, skipping..\n", jobSpecName) + shouldSkip = true + } + } + } + + if shouldSkip { + continue + } + fmt.Printf("Deploying jobspec: %s\n... \n", jobSpecStr) + _, err := api.withArg(jobSpecStr).exec(api.methods.CreateJob) if err != nil { - fmt.Println("Failed to deploy job spec:", specFragment, "Error:", err) + panic(fmt.Sprintf("Failed to deploy job spec: %s Error: %s", jobSpecStr, err)) } + } +} - // hard coded bridges for now - createBridgeIfDoesNotExist(api, "bridge-coinmetrics", "http://localhost:4001") - createBridgeIfDoesNotExist(api, "bridge-tiingo", "http://localhost:4002") - createBridgeIfDoesNotExist(api, "bridge-ncfx", "http://localhost:4003") +func createMercuryV3BootstrapJob( + verifierAddress common.Address, + feedName string, + feedID [32]byte, + chainID int64, +) (name string, jobSpecStr string) { + name = fmt.Sprintf("boot-%s", feedName) + fmt.Printf("Creating bootstrap job (%s):\nverifier address: %s\nfeed name: %s\nfeed ID: %x\nchain ID: %d\n", name, verifierAddress, feedName, feedID, chainID) + jobSpecStr = fmt.Sprintf(` +type = "bootstrap" +relay = "evm" +schemaVersion = 1 +name = "%s" +contractID = "%s" +feedID = "0x%x" +contractConfigTrackerPollInterval = "1s" + +[relayConfig] +chainID = %d +enableTriggerCapabillity = true + `, name, verifierAddress, feedID, chainID) - } + return +} + +func createMercuryV3Job( + ocrKeyBundleID string, + verifierAddress common.Address, + bridge string, + nodeCSAKey string, + feedName string, + feedID [32]byte, + linkFeedID [32]byte, + nativeFeedID [32]byte, + chainID int64, +) (name string, jobSpecStr string) { + name = fmt.Sprintf("mercury-%s", feedName) + fmt.Printf("Creating ocr2 job(%s):\nOCR key bundle ID: %s\nverifier address: %s\nbridge: %s\nnodeCSAKey: %s\nfeed name: %s\nfeed ID: %x\nlink feed ID: %x\nnative feed ID: %x\nchain ID: %d\n", name, ocrKeyBundleID, verifierAddress, bridge, nodeCSAKey, feedName, feedID, linkFeedID, nativeFeedID, chainID) + + jobSpecStr = fmt.Sprintf(` +type = "offchainreporting2" +schemaVersion = 1 +name = "mercury-%[1]s" +forwardingAllowed = false +maxTaskDuration = "1s" +contractID = "%[2]s" +feedID = "0x%[3]x" +contractConfigTrackerPollInterval = "1s" +ocrKeyBundleID = "%[4]s" +relay = "evm" +pluginType = "mercury" +transmitterID = "%[5]s" +observationSource = """ + price [type=bridge name="%[6]s" timeout="50ms" requestData=""]; + + benchmark_price [type=jsonparse path="result,mid" index=0]; + price -> benchmark_price; + + bid_price [type=jsonparse path="result,bid" index=1]; + price -> bid_price; + + ask_price [type=jsonparse path="result,ask" index=2]; + price -> ask_price; +""" + +[pluginConfig] +# Dummy pub key +serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" +linkFeedID = "0x%[7]x" +nativeFeedID = "0x%[8]x" +serverURL = "wss://unknown" + +[relayConfig] +enableTriggerCapabillity = true +chainID = "%[9]d" + `, + feedName, + verifierAddress, + feedID, + ocrKeyBundleID, + nodeCSAKey, + bridge, + linkFeedID, + nativeFeedID, + chainID, + ) + return } func createBridgeIfDoesNotExist(api *nodeAPI, name string, eaURL string) { + fmt.Printf("Creating bridge (%s): %s\n", name, eaURL) if doesBridgeExist(api, name) { fmt.Println("Bridge", name, "already exists, skipping creation") return @@ -155,87 +395,3 @@ func doesBridgeExist(api *nodeAPI, name string) bool { fmt.Printf("Found bridge: %s with URL: %s\n", b.Name, b.URL) return true } - -func genStreamsTriggerJobSpecs( - pubkeysPath string, - nodeListPath string, - templatesDir string, - - feedID string, - linkFeedID string, - nativeFeedID string, - - chainID int64, - fromBlock int64, - - verifierContractAddress string, - verifierProxyContractAddress string, -) (output [][]string) { - nodes := downloadNodeAPICredentials(nodeListPath) - nca := downloadNodePubKeys(nodeListPath, chainID, pubkeysPath) - lines, err := readLines(filepath.Join(templatesDir, streamsTriggerSpecTemplate)) - if err != nil { - PanicErr(err) - } - - for i := 0; i < len(nodes); i++ { - n := nca[i] - specLines := renderStreamsTriggerJobSpec( - lines, - - feedID, - linkFeedID, - nativeFeedID, - - chainID, - fromBlock, - - verifierContractAddress, - verifierProxyContractAddress, - - n, - ) - output = append(output, specLines) - } - - return output -} - -func renderStreamsTriggerJobSpec( - lines []string, - - feedID string, - linkFeedID string, - nativeFeedID string, - - chainID int64, - fromBlock int64, - - verifierContractAddress string, - verifierProxyContractAddress string, - - node NodeKeys, -) (output []string) { - chainIDStr := strconv.FormatInt(chainID, 10) - fromBlockStr := strconv.FormatInt(fromBlock, 10) - for _, l := range lines { - l = strings.Replace(l, "{{ feed_id }}", feedID, 1) - l = strings.Replace(l, "{{ link_feed_id }}", linkFeedID, 1) - l = strings.Replace(l, "{{ native_feed_id }}", nativeFeedID, 1) - - l = strings.Replace(l, "{{ chain_id }}", chainIDStr, 1) - l = strings.Replace(l, "{{ from_block }}", fromBlockStr, 1) - - // Verifier contract https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/integration_test.go#L111 - l = strings.Replace(l, "{{ contract_id }}", verifierContractAddress, 1) - // Ends up just being part of the name as documentation, it's the proxy to the verifier contract - l = strings.Replace(l, "{{ verifier_proxy_id }}", verifierProxyContractAddress, 1) - - // TransmitterID is the CSA key of the node since it's offchain - // https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/helpers_test.go#L219 - // https://github.com/smartcontractkit/chainlink-common/blob/9ee1e8cc8b9774c8f3eb92a722af5269469f46f4/pkg/types/mercury/types.go#L39 - l = strings.Replace(l, "{{ transmitter_id }}", node.CSAPublicKey, 1) - output = append(output, l) - } - return -} diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go index ea5ed744d2..4bb8aa275b 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go @@ -1,44 +1,47 @@ package src import ( - "strings" "testing" "github.com/gkampitakis/go-snaps/snaps" ) -func TestGenStreamsTriggerJobSpecs(t *testing.T) { - pubkeysPath := "./testdata/PublicKeys.json" - nodeListPath := "./testdata/NodeList.txt" - templatesDir := "../templates" - - feedID := "feed123" - linkFeedID := "linkfeed123" - nativeFeedID := "nativefeed123" - - chainID := int64(123456) - fromBlock := int64(10) - - verifierContractAddress := "verifier_contract_address" - verifierProxyContractAddress := "verifier_proxy_contract_address" +var ( + chainID = int64(123456) + feedID = [32]byte{0: 1} + feedName = "BTC/USD" + verifierAddress = [20]byte{0: 7} +) - output := genStreamsTriggerJobSpecs( - pubkeysPath, - nodeListPath, - templatesDir, +func TestCreateMercuryV3Job(t *testing.T) { + ocrKeyBundleID := "ocr_key_bundle_id" + nodeCSAKey := "node_csa_key" + bridgeName := "bridge_name" + linkFeedID := [32]byte{0: 2} + nativeFeedID := [32]byte{0: 3} + + _, output := createMercuryV3Job( + ocrKeyBundleID, + verifierAddress, + bridgeName, + nodeCSAKey, + feedName, feedID, linkFeedID, nativeFeedID, chainID, - fromBlock, - verifierContractAddress, - verifierProxyContractAddress, ) - prettyOutputs := []string{} - for _, o := range output { - prettyOutputs = append(prettyOutputs, strings.Join(o, "\n")) - } - testOutput := strings.Join(prettyOutputs, "\n\n-------------------------------------------------\n\n") - snaps.MatchSnapshot(t, testOutput) + snaps.MatchSnapshot(t, output) +} + +func TestCreateMercuryBootstrapJob(t *testing.T) { + _, output := createMercuryV3BootstrapJob( + verifierAddress, + feedName, + feedID, + chainID, + ) + + snaps.MatchSnapshot(t, output) } diff --git a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap index 40a5062b54..caecccb3ae 100755 --- a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap +++ b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap @@ -1,395 +1,96 @@ [TestGenStreamsTriggerJobSpecs - 1] -name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" -type = "offchainreporting2" -schemaVersion = 1 -forwardingAllowed = false -maxTaskDuration = "0s" -contractID = "verifier_contract_address" -relay = "evm" -pluginType = "mercury" -feedID = "feed123" -transmitterID = "csa_dbae6965bad0b0fa95ecc34a602eee1c0c570ddc29b56502e400d18574b8c3df" -observationSource = """ -// data source 1 -// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices -// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 -ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds1_benchmark [type=jsonparse path="data,mid"]; -ds1_bid [type=jsonparse path="data,bid"]; -ds1_ask [type=jsonparse path="data,ask"]; - -ds1_benchmark_multiply [type=multiply times=1000000000000000000]; -ds1_bid_multiply [type=multiply times=1000000000000000000]; -ds1_ask_multiply [type=multiply times=1000000000000000000]; -// data source 2 -ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds2_benchmark [type=jsonparse path="data,mid"]; -ds2_bid [type=jsonparse path="data,bid"]; -ds2_ask [type=jsonparse path="data,ask"]; - -ds2_benchmark_multiply [type=multiply times=1000000000000000000]; -ds2_bid_multiply [type=multiply times=1000000000000000000]; -ds2_ask_multiply [type=multiply times=1000000000000000000]; -// data source 3 -ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds3_benchmark [type=jsonparse path="data,mid"]; -ds3_bid [type=jsonparse path="data,bid"]; -ds3_ask [type=jsonparse path="data,ask"]; - -ds3_benchmark_multiply [type=multiply times=1000000000000000000]; -ds3_bid_multiply [type=multiply times=1000000000000000000]; -ds3_ask_multiply [type=multiply times=1000000000000000000]; -// benchmark -ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; -ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; -ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; -// The index is what determines how fields get assigned in a mercury report -// benchmark is always index 0 -// bid is always index 1 -// ask is always index 2 -// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info -benchmark_price [type=median allowedFaults=2 index=0]; -// bid -ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; -ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; -ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; -bid_price [type=median allowedFaults=2 index=1]; - -// ask -ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; -ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; -ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; -ask_price [type=median allowedFaults=2 index=2]; -""" - -[relayConfig] -chainID = "123456" -enableTriggerCapability = true -fromBlock = "10" - -[pluginConfig] -linkFeedID = "linkfeed123" -nativeFeedID = "nativefeed123" -# Dummy pub key -serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -# We don't need to specify a mercury server URL here since we're using this as a trigger instead -serverURL = "wss://unknown" - -------------------------------------------------- - -name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" type = "offchainreporting2" schemaVersion = 1 +name = "mercury-BTC/USD" forwardingAllowed = false -maxTaskDuration = "0s" -contractID = "verifier_contract_address" +maxTaskDuration = "1s" +contractID = "0x0700000000000000000000000000000000000000" +feedID = "0x0100000000000000000000000000000000000000000000000000000000000000" +contractConfigTrackerPollInterval = "1s" +ocrKeyBundleID = "ocr_key_bundle_id" relay = "evm" pluginType = "mercury" -feedID = "feed123" -transmitterID = "csa_c5cc655a9c19b69626519c4a72c44a94a3675daeba9c16cc23e010a7a6dac1be" +transmitterID = "0400000000000000000000000000000000000000000000000000000000000004" observationSource = """ -// data source 1 -// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices -// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 -ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds1_benchmark [type=jsonparse path="data,mid"]; -ds1_bid [type=jsonparse path="data,bid"]; -ds1_ask [type=jsonparse path="data,ask"]; - -ds1_benchmark_multiply [type=multiply times=1000000000000000000]; -ds1_bid_multiply [type=multiply times=1000000000000000000]; -ds1_ask_multiply [type=multiply times=1000000000000000000]; -// data source 2 -ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds2_benchmark [type=jsonparse path="data,mid"]; -ds2_bid [type=jsonparse path="data,bid"]; -ds2_ask [type=jsonparse path="data,ask"]; + price [type=bridge name="bridge_name" timeout="50ms" requestData=""]; -ds2_benchmark_multiply [type=multiply times=1000000000000000000]; -ds2_bid_multiply [type=multiply times=1000000000000000000]; -ds2_ask_multiply [type=multiply times=1000000000000000000]; -// data source 3 -ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds3_benchmark [type=jsonparse path="data,mid"]; -ds3_bid [type=jsonparse path="data,bid"]; -ds3_ask [type=jsonparse path="data,ask"]; + benchmark_price [type=jsonparse path="result,mid" index=0]; + price -> benchmark_price; -ds3_benchmark_multiply [type=multiply times=1000000000000000000]; -ds3_bid_multiply [type=multiply times=1000000000000000000]; -ds3_ask_multiply [type=multiply times=1000000000000000000]; -// benchmark -ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; -ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; -ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; -// The index is what determines how fields get assigned in a mercury report -// benchmark is always index 0 -// bid is always index 1 -// ask is always index 2 -// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info -benchmark_price [type=median allowedFaults=2 index=0]; + bid_price [type=jsonparse path="result,bid" index=1]; + price -> bid_price; -// bid -ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; -ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; -ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; -bid_price [type=median allowedFaults=2 index=1]; - -// ask -ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; -ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; -ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; -ask_price [type=median allowedFaults=2 index=2]; + ask_price [type=jsonparse path="result,ask" index=2]; + price -> ask_price; """ -[relayConfig] -chainID = "123456" -enableTriggerCapability = true -fromBlock = "10" - [pluginConfig] -linkFeedID = "linkfeed123" -nativeFeedID = "nativefeed123" # Dummy pub key serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -# We don't need to specify a mercury server URL here since we're using this as a trigger instead +linkFeedID = "0x0200000000000000000000000000000000000000000000000000000000000000" +nativeFeedID = "0x0300000000000000000000000000000000000000000000000000000000000000" serverURL = "wss://unknown" -------------------------------------------------- - -name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" -type = "offchainreporting2" -schemaVersion = 1 -forwardingAllowed = false -maxTaskDuration = "0s" -contractID = "verifier_contract_address" -relay = "evm" -pluginType = "mercury" -feedID = "feed123" -transmitterID = "csa_7407fc90c70895c0fb2bdf385e2e4918364bec1f7a74bad7fdf696bffafbcab8" -observationSource = """ -// data source 1 -// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices -// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 -ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds1_benchmark [type=jsonparse path="data,mid"]; -ds1_bid [type=jsonparse path="data,bid"]; -ds1_ask [type=jsonparse path="data,ask"]; - -ds1_benchmark_multiply [type=multiply times=1000000000000000000]; -ds1_bid_multiply [type=multiply times=1000000000000000000]; -ds1_ask_multiply [type=multiply times=1000000000000000000]; -// data source 2 -ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds2_benchmark [type=jsonparse path="data,mid"]; -ds2_bid [type=jsonparse path="data,bid"]; -ds2_ask [type=jsonparse path="data,ask"]; - -ds2_benchmark_multiply [type=multiply times=1000000000000000000]; -ds2_bid_multiply [type=multiply times=1000000000000000000]; -ds2_ask_multiply [type=multiply times=1000000000000000000]; -// data source 3 -ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds3_benchmark [type=jsonparse path="data,mid"]; -ds3_bid [type=jsonparse path="data,bid"]; -ds3_ask [type=jsonparse path="data,ask"]; - -ds3_benchmark_multiply [type=multiply times=1000000000000000000]; -ds3_bid_multiply [type=multiply times=1000000000000000000]; -ds3_ask_multiply [type=multiply times=1000000000000000000]; -// benchmark -ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; -ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; -ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; -// The index is what determines how fields get assigned in a mercury report -// benchmark is always index 0 -// bid is always index 1 -// ask is always index 2 -// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info -benchmark_price [type=median allowedFaults=2 index=0]; +[relayConfig] +enableTriggerCapabillity = true +chainID = "123456" + +--- -// bid -ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; -ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; -ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; -bid_price [type=median allowedFaults=2 index=1]; +[TestCreateMercuryBootstrapJob - 1] -// ask -ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; -ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; -ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; -ask_price [type=median allowedFaults=2 index=2]; -""" +type = "bootstrap" +relay = "evm" +schemaVersion = 1 +name = "boot-BTC/USD" +contractID = "0x0700000000000000000000000000000000000000" +feedID = "0x0100000000000000000000000000000000000000000000000000000000000000" +contractConfigTrackerPollInterval = "1s" [relayConfig] -chainID = "123456" -enableTriggerCapability = true -fromBlock = "10" +chainID = 123456 +enableTriggerCapabillity = true -[pluginConfig] -linkFeedID = "linkfeed123" -nativeFeedID = "nativefeed123" -# Dummy pub key -serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -# We don't need to specify a mercury server URL here since we're using this as a trigger instead -serverURL = "wss://unknown" +--- -------------------------------------------------- +[TestCreateMercuryV3Job - 1] -name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" type = "offchainreporting2" schemaVersion = 1 +name = "mercury-BTC/USD" forwardingAllowed = false -maxTaskDuration = "0s" -contractID = "verifier_contract_address" +maxTaskDuration = "1s" +contractID = "0x0700000000000000000000000000000000000000" +feedID = "0x0100000000000000000000000000000000000000000000000000000000000000" +contractConfigTrackerPollInterval = "1s" +ocrKeyBundleID = "ocr_key_bundle_id" relay = "evm" pluginType = "mercury" -feedID = "feed123" -transmitterID = "csa_ef55caf17eefc2a9d547b5a3978d396bd237c73af99cd849a4758701122e3cba" +transmitterID = "node_csa_key" observationSource = """ -// data source 1 -// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices -// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 -ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds1_benchmark [type=jsonparse path="data,mid"]; -ds1_bid [type=jsonparse path="data,bid"]; -ds1_ask [type=jsonparse path="data,ask"]; - -ds1_benchmark_multiply [type=multiply times=1000000000000000000]; -ds1_bid_multiply [type=multiply times=1000000000000000000]; -ds1_ask_multiply [type=multiply times=1000000000000000000]; -// data source 2 -ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds2_benchmark [type=jsonparse path="data,mid"]; -ds2_bid [type=jsonparse path="data,bid"]; -ds2_ask [type=jsonparse path="data,ask"]; + price [type=bridge name="bridge_name" timeout="50ms" requestData=""]; -ds2_benchmark_multiply [type=multiply times=1000000000000000000]; -ds2_bid_multiply [type=multiply times=1000000000000000000]; -ds2_ask_multiply [type=multiply times=1000000000000000000]; -// data source 3 -ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds3_benchmark [type=jsonparse path="data,mid"]; -ds3_bid [type=jsonparse path="data,bid"]; -ds3_ask [type=jsonparse path="data,ask"]; + benchmark_price [type=jsonparse path="result,mid" index=0]; + price -> benchmark_price; -ds3_benchmark_multiply [type=multiply times=1000000000000000000]; -ds3_bid_multiply [type=multiply times=1000000000000000000]; -ds3_ask_multiply [type=multiply times=1000000000000000000]; -// benchmark -ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; -ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; -ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; -// The index is what determines how fields get assigned in a mercury report -// benchmark is always index 0 -// bid is always index 1 -// ask is always index 2 -// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info -benchmark_price [type=median allowedFaults=2 index=0]; + bid_price [type=jsonparse path="result,bid" index=1]; + price -> bid_price; -// bid -ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; -ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; -ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; -bid_price [type=median allowedFaults=2 index=1]; - -// ask -ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; -ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; -ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; -ask_price [type=median allowedFaults=2 index=2]; + ask_price [type=jsonparse path="result,ask" index=2]; + price -> ask_price; """ -[relayConfig] -chainID = "123456" -enableTriggerCapability = true -fromBlock = "10" - [pluginConfig] -linkFeedID = "linkfeed123" -nativeFeedID = "nativefeed123" # Dummy pub key serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -# We don't need to specify a mercury server URL here since we're using this as a trigger instead +linkFeedID = "0x0200000000000000000000000000000000000000000000000000000000000000" +nativeFeedID = "0x0300000000000000000000000000000000000000000000000000000000000000" serverURL = "wss://unknown" -------------------------------------------------- - -name = "MATIC/USD-RefPrice-DFstaging-Premium-ArbitrumSepolia-001 | feed123 | verifier_proxy verifier_proxy_contract_address" -type = "offchainreporting2" -schemaVersion = 1 -forwardingAllowed = false -maxTaskDuration = "0s" -contractID = "verifier_contract_address" -relay = "evm" -pluginType = "mercury" -feedID = "feed123" -transmitterID = "csa_1b874ac2d54b966cec5a8358678ca6f030261aabf3372ce9dbea2d4eb9cdab3d" -observationSource = """ -// data source 1 -// https://docs.chain.link/data-streams/concepts/liquidity-weighted-prices -// https://github.com/smartcontractkit/ea-framework-js/blob/272846061bd0871da41d844672509aa7b7784d62/src/adapter/lwba.ts#L62 -ds1_payload [type=bridge name="bridge-coinmetrics" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds1_benchmark [type=jsonparse path="data,mid"]; -ds1_bid [type=jsonparse path="data,bid"]; -ds1_ask [type=jsonparse path="data,ask"]; - -ds1_benchmark_multiply [type=multiply times=1000000000000000000]; -ds1_bid_multiply [type=multiply times=1000000000000000000]; -ds1_ask_multiply [type=multiply times=1000000000000000000]; -// data source 2 -ds2_payload [type=bridge name="bridge-tiingo" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds2_benchmark [type=jsonparse path="data,mid"]; -ds2_bid [type=jsonparse path="data,bid"]; -ds2_ask [type=jsonparse path="data,ask"]; - -ds2_benchmark_multiply [type=multiply times=1000000000000000000]; -ds2_bid_multiply [type=multiply times=1000000000000000000]; -ds2_ask_multiply [type=multiply times=1000000000000000000]; -// data source 3 -ds3_payload [type=bridge name="bridge-ncfx" timeout="50s" requestData="{\\"data\\":{\\"from\\":\\"MATIC\\",\\"to\\":\\"USD\\"}}"]; -ds3_benchmark [type=jsonparse path="data,mid"]; -ds3_bid [type=jsonparse path="data,bid"]; -ds3_ask [type=jsonparse path="data,ask"]; - -ds3_benchmark_multiply [type=multiply times=1000000000000000000]; -ds3_bid_multiply [type=multiply times=1000000000000000000]; -ds3_ask_multiply [type=multiply times=1000000000000000000]; -// benchmark -ds1_payload -> ds1_benchmark -> ds1_benchmark_multiply -> benchmark_price; -ds2_payload -> ds2_benchmark -> ds2_benchmark_multiply -> benchmark_price; -ds3_payload -> ds3_benchmark -> ds3_benchmark_multiply -> benchmark_price; -// The index is what determines how fields get assigned in a mercury report -// benchmark is always index 0 -// bid is always index 1 -// ask is always index 2 -// See https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/relay/evm/mercury/v3/data_source.go#L225 for more info -benchmark_price [type=median allowedFaults=2 index=0]; - -// bid -ds1_payload -> ds1_bid -> ds1_bid_multiply -> bid_price; -ds2_payload -> ds2_bid -> ds2_bid_multiply -> bid_price; -ds3_payload -> ds3_bid -> ds3_bid_multiply -> bid_price; -bid_price [type=median allowedFaults=2 index=1]; - -// ask -ds1_payload -> ds1_ask -> ds1_ask_multiply -> ask_price; -ds2_payload -> ds2_ask -> ds2_ask_multiply -> ask_price; -ds3_payload -> ds3_ask -> ds3_ask_multiply -> ask_price; -ask_price [type=median allowedFaults=2 index=2]; -""" - [relayConfig] -chainID = "123456" -enableTriggerCapability = true -fromBlock = "10" - -[pluginConfig] -linkFeedID = "linkfeed123" -nativeFeedID = "nativefeed123" -# Dummy pub key -serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -# We don't need to specify a mercury server URL here since we're using this as a trigger instead -serverURL = "wss://unknown" +enableTriggerCapabillity = true +chainID = "123456" + --- From 9c497737bb9755158a420509021a7525bd88c6f9 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:15:05 -0700 Subject: [PATCH 19/33] Get oracles to successfully connect to each other --- .../src/03_deploy_streams_trigger_cmd.go | 134 ++++++++++++++++-- .../src/03_deploy_streams_trigger_cmd_test.go | 8 +- .../03_deploy_streams_trigger_cmd_test.snap | 1 + 3 files changed, 131 insertions(+), 12 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index d1ebf842b9..c9576b2f65 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -15,18 +15,30 @@ import ( "encoding/json" "flag" "fmt" + "math" "math/big" "os" + "time" "net/url" "github.com/ethereum/go-ethereum/common" + "github.com/shopspring/decimal" "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/libocr/offchainreporting2plus/confighelper" + "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3confighelper" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + mercurytypes "github.com/smartcontractkit/chainlink-common/pkg/types/mercury" + datastreamsmercury "github.com/smartcontractkit/chainlink-data-streams/mercury" + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" "github.com/smartcontractkit/chainlink/v2/core/bridges" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/fee_manager" // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/reward_manager" @@ -145,7 +157,7 @@ func setupMercuryV03(env helpers.Environment, nodeListPath string, ocrConfigFile nodes := downloadNodeAPICredentials(nodeListPath) fmt.Printf("Generating OCR3 config\n") - ocrConfig := generateOCR3Config(nodeListPath, ocrConfigFilePath, chainId, pubKeysPath, OffChainTransmitter, kbIndex) + ocrConfig := generateMercuryOCR2Config(nca) for _, feed := range feeds { fmt.Println("Configuring feeds...") @@ -155,10 +167,11 @@ func setupMercuryV03(env helpers.Environment, nodeListPath string, ocrConfigFile fmt.Printf("BridgeURL: %s\n", feed.bridgeUrl) fmt.Printf("Setting verifier config\n") - verifier.SetConfig(env.Owner, + verifier.SetConfig( + env.Owner, feed.id, ocrConfig.Signers, - ocrConfig.OffChainTransmitters, + ocrConfig.Transmitters, ocrConfig.F, ocrConfig.OnchainConfig, ocrConfig.OffchainConfigVersion, @@ -235,6 +248,7 @@ func deployOCR2JobSpecsForFeed(nca []NodeKeys, nodes []*node, verifier *verifier } else { jobSpecName, jobSpecStr = createMercuryV3Job( n.OCR2BundleID, + fmt.Sprintf("%s@%s:%s", nca[0].P2PPeerID, "app-node1", "6690"), verifier.Address(), feed.bridgeName, n.CSAPublicKey, @@ -300,6 +314,7 @@ enableTriggerCapabillity = true func createMercuryV3Job( ocrKeyBundleID string, + bootstrapHost string, verifierAddress common.Address, bridge string, nodeCSAKey string, @@ -316,17 +331,18 @@ func createMercuryV3Job( type = "offchainreporting2" schemaVersion = 1 name = "mercury-%[1]s" +p2pv2Bootstrappers = ["%[2]s"] forwardingAllowed = false maxTaskDuration = "1s" -contractID = "%[2]s" -feedID = "0x%[3]x" +contractID = "%[3]s" +feedID = "0x%[4]x" contractConfigTrackerPollInterval = "1s" -ocrKeyBundleID = "%[4]s" +ocrKeyBundleID = "%[5]s" relay = "evm" pluginType = "mercury" -transmitterID = "%[5]s" +transmitterID = "%[6]s" observationSource = """ - price [type=bridge name="%[6]s" timeout="50ms" requestData=""]; + price [type=bridge name="%[7]s" timeout="50ms" requestData=""]; benchmark_price [type=jsonparse path="result,mid" index=0]; price -> benchmark_price; @@ -341,15 +357,16 @@ observationSource = """ [pluginConfig] # Dummy pub key serverPubKey = "11a34b5187b1498c0ccb2e56d5ee8040a03a4955822ed208749b474058fc3f9c" -linkFeedID = "0x%[7]x" -nativeFeedID = "0x%[8]x" +linkFeedID = "0x%[8]x" +nativeFeedID = "0x%[9]x" serverURL = "wss://unknown" [relayConfig] enableTriggerCapabillity = true -chainID = "%[9]d" +chainID = "%[10]d" `, feedName, + bootstrapHost, verifierAddress, feedID, ocrKeyBundleID, @@ -395,3 +412,98 @@ func doesBridgeExist(api *nodeAPI, name string) bool { fmt.Printf("Found bridge: %s with URL: %s\n", b.Name, b.URL) return true } + +func generateMercuryOCR2Config(nca []NodeKeys) MercuryOCR2Config { + f := uint8(1) + rawOnchainConfig := mercurytypes.OnchainConfig{ + Min: big.NewInt(0), + Max: big.NewInt(math.MaxInt64), + } + rawReportingPluginConfig := datastreamsmercury.OffchainConfig{ + ExpirationWindow: 1, + BaseUSDFee: decimal.NewFromInt(100), + } + + onchainConfig, err := (datastreamsmercury.StandardOnchainConfigCodec{}).Encode(rawOnchainConfig) + helpers.PanicErr(err) + reportingPluginConfig, err := json.Marshal(rawReportingPluginConfig) + helpers.PanicErr(err) + + onchainPubKeys := []common.Address{} + for _, n := range nca { + onchainPubKeys = append(onchainPubKeys, common.HexToAddress(n.OCR2OnchainPublicKey)) + } + + offchainPubKeysBytes := []ocrtypes.OffchainPublicKey{} + for _, n := range nca { + + pkBytesFixed := strToBytes32(n.OCR2OffchainPublicKey) + offchainPubKeysBytes = append(offchainPubKeysBytes, ocrtypes.OffchainPublicKey(pkBytesFixed)) + } + + configPubKeysBytes := []ocrtypes.ConfigEncryptionPublicKey{} + for _, n := range nca { + pkBytesFixed := strToBytes32(n.OCR2ConfigPublicKey) + configPubKeysBytes = append(configPubKeysBytes, ocrtypes.ConfigEncryptionPublicKey(pkBytesFixed)) + } + + identities := []confighelper.OracleIdentityExtra{} + for index := range nca { + transmitterAccount := ocrtypes.Account(fmt.Sprintf("%x", nca[index].CSAPublicKey[:])) + + identities = append(identities, confighelper.OracleIdentityExtra{ + OracleIdentity: confighelper.OracleIdentity{ + OnchainPublicKey: onchainPubKeys[index][:], + OffchainPublicKey: offchainPubKeysBytes[index], + PeerID: nca[index].P2PPeerID, + TransmitAccount: transmitterAccount, + }, + ConfigEncryptionPublicKey: configPubKeysBytes[index], + }) + } + + signers, _, _, onchainConfig, offchainConfigVersion, offchainConfig, err := ocr3confighelper.ContractSetConfigArgsForTestsMercuryV02( + 2*time.Second, // DeltaProgress + 20*time.Second, // DeltaResend + 400*time.Millisecond, // DeltaInitial + 100*time.Millisecond, // DeltaRound + 0, // DeltaGrace + 300*time.Millisecond, // DeltaCertifiedCommitRequest + 1*time.Minute, // DeltaStage + 100, // rMax + []int{len(identities)}, // S + identities, + reportingPluginConfig, // reportingPluginConfig []byte, + 250*time.Millisecond, // Max duration observation + int(f), // f + onchainConfig, + ) + signerAddresses, err := evm.OnchainPublicKeyToAddress(signers) + PanicErr(err) + + var offChainTransmitters [][32]byte + for _, n := range nca { + fmt.Println("CSAPublicKey", n.CSAPublicKey) + offChainTransmitters = append(offChainTransmitters, strToBytes32(n.CSAPublicKey)) + } + + config := MercuryOCR2Config{ + Signers: signerAddresses, + Transmitters: offChainTransmitters, + F: f, + OnchainConfig: onchainConfig, + OffchainConfigVersion: offchainConfigVersion, + OffchainConfig: offchainConfig, + } + + return config +} + +type MercuryOCR2Config struct { + Signers []common.Address + Transmitters [][32]byte + F uint8 + OnchainConfig []byte + OffchainConfigVersion uint64 + OffchainConfig []byte +} diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go index 4bb8aa275b..5606789fc5 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd_test.go @@ -1,6 +1,7 @@ package src import ( + "net/url" "testing" "github.com/gkampitakis/go-snaps/snaps" @@ -15,13 +16,18 @@ var ( func TestCreateMercuryV3Job(t *testing.T) { ocrKeyBundleID := "ocr_key_bundle_id" - nodeCSAKey := "node_csa_key" + nodeCSAKey := "node_csa_key" bridgeName := "bridge_name" linkFeedID := [32]byte{0: 2} nativeFeedID := [32]byte{0: 3} + u, err := url.Parse("https://crib-henry-keystone-node1.main.stage.cldev.sh") + if err != nil { + t.Fatal(err) + } _, output := createMercuryV3Job( ocrKeyBundleID, + u.Hostname(), verifierAddress, bridgeName, nodeCSAKey, diff --git a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap index caecccb3ae..bc6caa0d69 100755 --- a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap +++ b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap @@ -60,6 +60,7 @@ enableTriggerCapabillity = true type = "offchainreporting2" schemaVersion = 1 name = "mercury-BTC/USD" +p2pv2Bootstrappers = ["crib-henry-keystone-node1.main.stage.cldev.sh"] forwardingAllowed = false maxTaskDuration = "1s" contractID = "0x0700000000000000000000000000000000000000" From 7abd46d6c0521f038038031cebd905d686348a97 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:59:03 -0700 Subject: [PATCH 20/33] Keep OCR3 and OCR2 config separate --- .../keystone/src/01_deploy_contracts_cmd.go | 1 - .../keystone/src/88_gen_ocr3_config.go | 47 ++++--------------- .../keystone/src/88_gen_ocr3_config_test.go | 2 +- .../88_gen_ocr3_config_test.snap | 10 ++-- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index f3345f6b6d..c4b1afde12 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -106,7 +106,6 @@ func deploy( configFile, env.ChainID, publicKeys, - OnChainTransmitter, ) if dryRun { diff --git a/core/scripts/keystone/src/88_gen_ocr3_config.go b/core/scripts/keystone/src/88_gen_ocr3_config.go index aed395acd3..b5973a775f 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config.go @@ -64,11 +64,8 @@ type NodeKeys struct { } type orc2drOracleConfig struct { - Signers [][]byte - // populated when transmitterType == OnChainTransmitter - Transmitters []common.Address - // populated when transmitterType == OffChainTransmitter - OffChainTransmitters [][32]byte + Signers [][]byte + Transmitters []common.Address F uint8 OnchainConfig []byte OffchainConfigVersion uint64 @@ -107,17 +104,10 @@ func mustReadConfig(fileName string) (output TopLevelConfigSource) { return mustParseJSON[TopLevelConfigSource](fileName) } -type TransmitterType string - -const ( - OnChainTransmitter TransmitterType = "onchain" - OffChainTransmitter TransmitterType = "offchain" -) - -func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKeysPath string, transmitterType TransmitterType, kbIndex ...int) orc2drOracleConfig { +func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKeysPath string) orc2drOracleConfig { topLevelCfg := mustReadConfig(configFile) cfg := topLevelCfg.OracleConfig - nca := downloadNodePubKeys(nodeList, chainID, pubKeysPath, kbIndex...) + nca := downloadNodePubKeys(nodeList, chainID, pubKeysPath) onchainPubKeys := [][]byte{} allPubKeys := map[string]any{} @@ -144,7 +134,6 @@ func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKe offchainPubKeysBytes := []types.OffchainPublicKey{} for _, n := range nca { - pkBytesFixed := strToBytes32(n.OCR2OffchainPublicKey) offchainPubKeysBytes = append(offchainPubKeysBytes, types.OffchainPublicKey(pkBytesFixed)) } @@ -157,20 +146,12 @@ func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKe identities := []confighelper.OracleIdentityExtra{} for index := range nca { - var transmitterAccount types.Account - if transmitterType == OnChainTransmitter { - transmitterAccount = types.Account(nca[index].EthAddress) - } - if transmitterType == OffChainTransmitter { - transmitterAccount = types.Account(fmt.Sprintf("%x", nca[index].CSAPublicKey[:])) - } - identities = append(identities, confighelper.OracleIdentityExtra{ OracleIdentity: confighelper.OracleIdentity{ OnchainPublicKey: onchainPubKeys[index][:], OffchainPublicKey: offchainPubKeysBytes[index], PeerID: nca[index].P2PPeerID, - TransmitAccount: transmitterAccount, + TransmitAccount: types.Account(nca[index].EthAddress), }, ConfigEncryptionPublicKey: configPubKeysBytes[index], }) @@ -202,26 +183,16 @@ func generateOCR3Config(nodeList string, configFile string, chainID int64, pubKe configSigners = append(configSigners, signer) } + transmitterAddresses, err := evm.AccountToAddress(transmitters) + PanicErr(err) + config := orc2drOracleConfig{ Signers: configSigners, F: f, OnchainConfig: onchainConfig, OffchainConfigVersion: offchainConfigVersion, OffchainConfig: offchainConfig, - } - - if transmitterType == OnChainTransmitter { - transmitterAddresses, err := evm.AccountToAddress(transmitters) - PanicErr(err) - config.Transmitters = transmitterAddresses - } - if transmitterType == OffChainTransmitter { - var offChainTransmitters [][32]byte - for _, n := range nca { - fmt.Println("CSAPublicKey", n.CSAPublicKey) - offChainTransmitters = append(offChainTransmitters, strToBytes32(n.CSAPublicKey)) - } - config.OffChainTransmitters = offChainTransmitters + Transmitters: transmitterAddresses, } return config diff --git a/core/scripts/keystone/src/88_gen_ocr3_config_test.go b/core/scripts/keystone/src/88_gen_ocr3_config_test.go index 19c6bc50a0..da22397c1c 100644 --- a/core/scripts/keystone/src/88_gen_ocr3_config_test.go +++ b/core/scripts/keystone/src/88_gen_ocr3_config_test.go @@ -10,7 +10,7 @@ import ( func TestGenerateOCR3Config(t *testing.T) { // Generate OCR3 config - config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 1337, "./testdata/PublicKeys.json", OnChainTransmitter) + config := generateOCR3Config(".cache/NodeList.txt", "./testdata/SampleConfig.json", 1337, "./testdata/PublicKeys.json") matchOffchainConfig := match.Custom("OffchainConfig", func(s any) (any, error) { // coerce the value to a string diff --git a/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap b/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap index 8b1a0baecc..295daf4f9c 100755 --- a/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap +++ b/core/scripts/keystone/src/__snapshots__/88_gen_ocr3_config_test.snap @@ -6,11 +6,11 @@ "OffchainConfigVersion": 30, "OnchainConfig": "0x", "Signers": [ - "0xbB9dA64fEfbAe323B45Ec98970863279824cf08D", - "0x26F04f488F6383Cd4099b7aEF16c0DC2e553aDDA", - "0xD754a628C5B34D027e4ccd8d90B54b9403A9B5f2", - "0xe366aEd6fe28F47cC2c463Cda40d4BB8573e15D4", - "0x405Aeb4fa9359A7338217bFc4Fdc77C886A6a6Ee" + "011400bb9da64fefbae323b45ec98970863279824cf08d", + "01140026f04f488f6383cd4099b7aef16c0dc2e553adda", + "011400d754a628c5b34d027e4ccd8d90b54b9403a9b5f2", + "011400e366aed6fe28f47cc2c463cda40d4bb8573e15d4", + "011400405aeb4fa9359a7338217bfc4fdc77c886a6a6ee" ], "Transmitters": [ "0x9DfB1E962Fc6363087fB60A131aba2b7884dAD97", From e8883d1159f02fa154e1241998154e669a0c48b5 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:42:25 -0700 Subject: [PATCH 21/33] Add goreleaser setup for mock ea --- .../src/external-adapter/.goreleaser.yaml | 20 +++++++ .../external-adapter/99_external_adapter.go | 52 ++++++++++++++++--- .../keystone/src/external-adapter/Dockerfile | 5 ++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 core/scripts/keystone/src/external-adapter/.goreleaser.yaml create mode 100644 core/scripts/keystone/src/external-adapter/Dockerfile diff --git a/core/scripts/keystone/src/external-adapter/.goreleaser.yaml b/core/scripts/keystone/src/external-adapter/.goreleaser.yaml new file mode 100644 index 0000000000..5c2141dd92 --- /dev/null +++ b/core/scripts/keystone/src/external-adapter/.goreleaser.yaml @@ -0,0 +1,20 @@ +project_name: kiab-mock-external-adapter +version: 2 + +builds: + - id: linux-amd64 + goos: + - linux + goarch: + - amd64 + +dockers: + - id: linux-amd64 + use: buildx + goos: linux + goarch: amd64 + image_templates: + - "{{ .Env.IMAGE }}" + +snapshot: + version_template: "{{ .ProjectName }}-{{ .ShortCommit }}" diff --git a/core/scripts/keystone/src/external-adapter/99_external_adapter.go b/core/scripts/keystone/src/external-adapter/99_external_adapter.go index 4c768ce4cc..5335bbaf1d 100644 --- a/core/scripts/keystone/src/external-adapter/99_external_adapter.go +++ b/core/scripts/keystone/src/external-adapter/99_external_adapter.go @@ -3,27 +3,65 @@ package main // Taken from https://github.com/smartcontractkit/chainlink/blob/4d5fc1943bd6a60b49cbc3d263c0aa47dc3cecb7/core/services/ocr2/plugins/mercury/integration_test.go#L1055 import ( "fmt" + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" "math/rand" "net" "net/http" "net/http/httptest" + "os" + "strconv" ) func main() { - // Simulating MATIC/USD - initialValue := 0.4 - pctBounds := 0.3 + // get initial value from env + btcUsdInitialValue := 0.0 + btcUsdInitialValueEnv := os.Getenv("BTCUSD_INITIAL_VALUE") + linkInitialValue := 0.0 + linkInitialValueEnv := os.Getenv("LINK_INITIAL_VALUE") + nativeInitialValue := 0.0 + nativeInitialValueEnv := os.Getenv("NATIVE_INITIAL_VALUE") + + if btcUsdInitialValueEnv == "" { + fmt.Println("INITIAL_VALUE not set, using default value") + btcUsdInitialValue = 1000 + } else { + fmt.Println("INITIAL_VALUE set to ", btcUsdInitialValueEnv) + val, err := strconv.ParseFloat(btcUsdInitialValueEnv, 64) + helpers.PanicErr(err) + btcUsdInitialValue = val + } + + if linkInitialValueEnv == "" { + fmt.Println("LINK_INITIAL_VALUE not set, using default value") + linkInitialValue = 11.0 + } else { + fmt.Println("LINK_INITIAL_VALUE set to ", linkInitialValueEnv) + val, err := strconv.ParseFloat(linkInitialValueEnv, 64) + helpers.PanicErr(err) + linkInitialValue = val + } + + if nativeInitialValueEnv == "" { + fmt.Println("NATIVE_INITIAL_VALUE not set, using default value") + nativeInitialValue = 2400.0 + } else { + fmt.Println("NATIVE_INITIAL_VALUE set to ", nativeInitialValueEnv) + val, err := strconv.ParseFloat(nativeInitialValueEnv, 64) + helpers.PanicErr(err) + nativeInitialValue = val + } - externalAdapter(initialValue, "4001", pctBounds) - externalAdapter(initialValue, "4002", pctBounds) - externalAdapter(initialValue, "4003", pctBounds) + pctBounds := 0.3 + externalAdapter(btcUsdInitialValue, "4001", pctBounds) + externalAdapter(linkInitialValue, "4002", pctBounds) + externalAdapter(nativeInitialValue, "4003", pctBounds) select {} } func externalAdapter(initialValue float64, port string, pctBounds float64) *httptest.Server { // Create a custom listener on the specified port - listener, err := net.Listen("tcp", "localhost:"+port) + listener, err := net.Listen("tcp", "0.0.0.0:"+port) if err != nil { panic(err) } diff --git a/core/scripts/keystone/src/external-adapter/Dockerfile b/core/scripts/keystone/src/external-adapter/Dockerfile new file mode 100644 index 0000000000..9242528f46 --- /dev/null +++ b/core/scripts/keystone/src/external-adapter/Dockerfile @@ -0,0 +1,5 @@ +FROM scratch + +ENTRYPOINT ["/kiab-mock-external-adapter"] + +COPY kiab-mock-external-adapter / From efe16570a041a42e24ae49a19bc3e3e173972f9b Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:42:45 -0700 Subject: [PATCH 22/33] Add support for updating bridges --- .../src/03_deploy_streams_trigger_cmd.go | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index c9576b2f65..1e3c251a92 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -69,19 +69,19 @@ var feeds = []feed{ v3FeedID([32]byte{5: 1}), "BTC/USD", "mock-bridge-btc", - "http://localhost:4000", + "http://external-adapter:4001", }, { v3FeedID([32]byte{5: 2}), "LINK/USD", "mock-bridge-link", - "http://localhost:4001", + "http://external-adapter:4002", }, { v3FeedID([32]byte{5: 3}), "NATIVE/USD", "mock-bridge-native", - "http://localhost:4002", + "http://external-adapter:4003", }, } @@ -237,7 +237,7 @@ func deployOCR2JobSpecsForFeed(nca []NodeKeys, nodes []*node, verifier *verifier jobSpecName := "" jobSpecStr := "" - createBridgeIfDoesNotExist(api, feed.bridgeName, feed.bridgeUrl) + createBridgeIfDoesNotExist(api, feed.bridgeName, feed.bridgeUrl, force) if i == 0 { jobSpecName, jobSpecStr = createMercuryV3BootstrapJob( verifier.Address(), @@ -379,13 +379,7 @@ chainID = "%[10]d" return } -func createBridgeIfDoesNotExist(api *nodeAPI, name string, eaURL string) { - fmt.Printf("Creating bridge (%s): %s\n", name, eaURL) - if doesBridgeExist(api, name) { - fmt.Println("Bridge", name, "already exists, skipping creation") - return - } - +func createBridgeIfDoesNotExist(api *nodeAPI, name string, eaURL string, force bool) { u, err := url.Parse(eaURL) url := models.WebURL(*u) // Confirmations and MinimumContractPayment are not used, so we can leave them as 0 @@ -393,12 +387,25 @@ func createBridgeIfDoesNotExist(api *nodeAPI, name string, eaURL string) { Name: bridges.MustParseBridgeName(name), URL: url, } - payload, err := json.Marshal(b) + payloadb, err := json.Marshal(b) helpers.PanicErr(err) + payload := string(payloadb) - resp := api.withArg(string(payload)).mustExec(api.methods.CreateBridge) - resource := mustJSON[presenters.BridgeResource](resp) - fmt.Printf("Created bridge: %s %s\n", resource.Name, resource.URL) + fmt.Printf("Creating bridge (%s): %s\n", name, eaURL) + if doesBridgeExist(api, name) { + if force { + fmt.Println("Force flag is set, updating existing bridge") + api.withArgs(name, payload).mustExec(api.methods.UpdateBridge) + fmt.Println("Updated bridge", name) + } else { + fmt.Println("Bridge", name, "already exists, skipping creation") + return + } + } else { + resp := api.withArg(payload).mustExec(api.methods.CreateBridge) + resource := mustJSON[presenters.BridgeResource](resp) + fmt.Printf("Created bridge: %s %s\n", resource.Name, resource.URL) + } } func doesBridgeExist(api *nodeAPI, name string) bool { From 9e6eb11c68a648c38565f2a9b77c4d403477aed8 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:43:14 -0700 Subject: [PATCH 23/33] Add UpdateBridge CLI command --- core/cmd/bridge_commands.go | 23 +++++++++++++++++ core/cmd/bridge_commands_test.go | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/core/cmd/bridge_commands.go b/core/cmd/bridge_commands.go index 398d466c43..cd314b2321 100644 --- a/core/cmd/bridge_commands.go +++ b/core/cmd/bridge_commands.go @@ -128,6 +128,29 @@ func (s *Shell) CreateBridge(c *cli.Context) (err error) { return s.renderAPIResponse(resp, &BridgePresenter{}) } +func (s *Shell) UpdateBridge(c *cli.Context) (err error) { + if !c.Args().Present() { + return s.errorOut(errors.New("must pass the name of the bridge to be updated")) + } + bridgeName := c.Args().First() + buf, err := getBufferFromJSON(c.Args().Get(1)) + if err != nil { + return s.errorOut(err) + } + + resp, err := s.HTTP.Patch(s.ctx(), "/v2/bridge_types/"+bridgeName, buf) + if err != nil { + return s.errorOut(err) + } + defer func() { + if cerr := resp.Body.Close(); cerr != nil { + err = multierr.Append(err, cerr) + } + }() + + return s.renderAPIResponse(resp, &BridgePresenter{}) +} + // RemoveBridge removes a specific Bridge by name. func (s *Shell) RemoveBridge(c *cli.Context) (err error) { if !c.Args().Present() { diff --git a/core/cmd/bridge_commands_test.go b/core/cmd/bridge_commands_test.go index f05aac52cd..04352b0d5d 100644 --- a/core/cmd/bridge_commands_test.go +++ b/core/cmd/bridge_commands_test.go @@ -3,6 +3,7 @@ package cmd_test import ( "bytes" "flag" + "fmt" "testing" "time" @@ -191,3 +192,44 @@ func TestShell_RemoveBridge(t *testing.T) { assert.Equal(t, bt.URL.String(), p.URL) assert.Equal(t, bt.Confirmations, p.Confirmations) } +func TestShell_UpdateBridge(t *testing.T) { + t.Parallel() + + app := startNewApplicationV2(t, nil) + client, _ := app.NewShellAndRenderer() + name := testutils.RandomizeName("updatebridge") + + bt := &bridges.BridgeType{ + Name: bridges.MustParseBridgeName(name), + URL: cltest.WebURL(t, "https://testing.com/bridges"), + Confirmations: 0, + } + require.NoError(t, app.BridgeORM().CreateBridgeType(testutils.Context(t), bt)) + tests := []struct { + name string + args []string + errored bool + }{ + {"NoArgs", []string{}, true}, + {"OnlyName", []string{name}, true}, + {"ValidUpdate", []string{name, fmt.Sprintf(`{ "name": "%s", "url": "http://localhost:3000/updated" }`, name)}, false}, + {"InvalidJSON", []string{name, `{ "url": "http://localhost:3000/updated"`}, true}, + } + + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + set := flag.NewFlagSet("bridge", 0) + flagSetApplyFromAction(client.UpdateBridge, set, "") + + require.NoError(t, set.Parse(test.args)) + + c := cli.NewContext(nil, set, nil) + if test.errored { + assert.Error(t, client.UpdateBridge(c)) + } else { + assert.Nil(t, client.UpdateBridge(c)) + } + }) + } +} From f52f12847af1c1c1722eb26d352512bf598eb47a Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:44:13 -0700 Subject: [PATCH 24/33] Cleanup comments --- .../keystone/src/03_deploy_streams_trigger_cmd.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index 1e3c251a92..ea5909b32f 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -40,8 +40,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" - // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/fee_manager" - // "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/reward_manager" verifierContract "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/verifier" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/llo-feeds/generated/verifier_proxy" @@ -217,14 +215,6 @@ func deployMercuryV03Contracts(env helpers.Environment) (linkToken *link_token_i tx, err = verifierProxy.InitializeVerifier(env.Owner, verifierAddress) confirmTx(tx, err) - // rewardManagerAddr, _, rewardManager, err := reward_manager.DeployRewardManager(env.Owner, env.Ec, linkTokenAddress) - // PanicErr(err) - // feeManagerAddr, _, _, err := fee_manager.DeployFeeManager(env.Owner, env.Ec, linkTokenAddress, nativeTokenAddress, verifierProxyAddr, rewardManagerAddr) - // PanicErr(err) - // _, err = verifierProxy.SetFeeManager(env.Owner, feeManagerAddr) - // PanicErr(err) - // _, err = rewardManager.SetFeeManager(env.Owner, feeManagerAddr) - // PanicErr(err) return } @@ -490,7 +480,6 @@ func generateMercuryOCR2Config(nca []NodeKeys) MercuryOCR2Config { var offChainTransmitters [][32]byte for _, n := range nca { - fmt.Println("CSAPublicKey", n.CSAPublicKey) offChainTransmitters = append(offChainTransmitters, strToBytes32(n.CSAPublicKey)) } From 1c25c1fa4aac7d7aa3938c0816a71214d275e5a8 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:21:26 -0700 Subject: [PATCH 25/33] Fix typo --- core/scripts/keystone/src/01_deploy_contracts_cmd.go | 2 +- .../keystone/src/06_provision_capabilities_registry.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/scripts/keystone/src/01_deploy_contracts_cmd.go b/core/scripts/keystone/src/01_deploy_contracts_cmd.go index c4b1afde12..83395bcfb0 100644 --- a/core/scripts/keystone/src/01_deploy_contracts_cmd.go +++ b/core/scripts/keystone/src/01_deploy_contracts_cmd.go @@ -19,7 +19,7 @@ import ( type deployedContracts struct { OCRContract common.Address `json:"ocrContract"` ForwarderContract common.Address `json:"forwarderContract"` - CapabilityRegsitry common.Address `json:"capabilityRegistry"` + CapabilityRegistry common.Address `json:"capabilityRegistry"` // The block number of the transaction that set the config on the OCR3 contract. We use this to replay blocks from this point on // when we load the OCR3 job specs on the nodes. SetConfigTxBlock uint64 `json:"setConfigTxBlock"` diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index eef45e1f4f..9bc91be92f 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -102,18 +102,18 @@ func getOrDeployCapabilitiesRegistry(ctx context.Context, artefactsDir string, e panic(err) } - if contracts.CapabilityRegsitry.String() == "0x" { + if contracts.CapabilityRegistry.String() == (common.Address{}).String() { _, tx, capabilitiesRegistry, innerErr := kcr.DeployCapabilitiesRegistry(env.Owner, env.Ec) if innerErr != nil { panic(innerErr) } helpers.ConfirmContractDeployed(ctx, env.Ec, tx, env.ChainID) - contracts.CapabilityRegsitry = capabilitiesRegistry.Address() + contracts.CapabilityRegistry = capabilitiesRegistry.Address() WriteDeployedContracts(contracts, artefactsDir) return capabilitiesRegistry } else { - capabilitiesRegistry, innerErr := kcr.NewCapabilitiesRegistry(contracts.CapabilityRegsitry, env.Ec) + capabilitiesRegistry, innerErr := kcr.NewCapabilitiesRegistry(contracts.CapabilityRegistry, env.Ec) if innerErr != nil { panic(innerErr) } From fa9ab1a9abfb96c79571c5cabd1599fadf75c6eb Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:23:28 -0700 Subject: [PATCH 26/33] Add revert detection and revert reason extraction --- .../keystone/src/88_capabilities_registry.go | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go index bf9082008b..fe03a4b5b3 100644 --- a/core/scripts/keystone/src/88_capabilities_registry.go +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -31,8 +31,59 @@ type CapabilityRegistryProvisioner struct { env helpers.Environment } -func NewCapabilityRegistryProvisioner(reg *kcr.CapabilitiesRegistry) *CapabilityRegistryProvisioner { - return &CapabilityRegistryProvisioner{reg: reg} + +func extractRevertReason(errData string, a abi.ABI) (string, string, error) { + data, err := hex.DecodeString(errData[2:]) + if err != nil { + return "", "", err + } + + for errName, abiError := range a.Errors { + if bytes.Equal(data[:4], abiError.ID.Bytes()[:4]) { + // Found a matching error + v, err := abiError.Unpack(data) + if err != nil { + return "", "", err + } + b, err := json.Marshal(v) + if err != nil { + return "", "", err + } + return errName, string(b), nil + } + } + return "", "", fmt.Errorf("revert Reason could not be found for given abistring") +} + +func (c *CapabilityRegistryProvisioner) testCallContract(method string, args ...interface{}) { + abi := evmtypes.MustGetABI(kcr.CapabilitiesRegistryABI) + data, err := abi.Pack(method, args...) + helpers.PanicErr(err) + cAddress := c.reg.Address() + gasPrice, err := c.env.Ec.SuggestGasPrice(context.Background()) + helpers.PanicErr(err) + + msg := ethereum.CallMsg{ + From: c.env.Owner.From, + To: &cAddress, + Data: data, + Gas: 10_000_000, + GasPrice: gasPrice, + } + _, err = c.env.Ec.CallContract(context.Background(), msg, nil) + if err != nil { + if err.Error() == "execution reverted" { + rpcError, ierr := evmclient.ExtractRPCError(err) + helpers.PanicErr(ierr) + reason, abiErr, ierr := extractRevertReason(rpcError.Data.(string), abi) + helpers.PanicErr(ierr) + + e := fmt.Errorf("failed to call %s: reason: %s reasonargs: %s", method, reason, abiErr) + helpers.PanicErr(e) + } + helpers.PanicErr(err) + } + } // AddCapabilities takes a capability set and provisions it in the registry. From efccb8b9c5bbc06636d36c0191e2807af40e04b8 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:23:59 -0700 Subject: [PATCH 27/33] WIP: Sendtx helper --- .../keystone/src/88_capabilities_registry.go | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go index fe03a4b5b3..57ce5c4206 100644 --- a/core/scripts/keystone/src/88_capabilities_registry.go +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -1,23 +1,31 @@ package src import ( + "bytes" "context" "fmt" "log" + "math/big" "time" + "github.com/ethereum/go-ethereum/accounts/abi" + "strings" "encoding/hex" + "encoding/json" + "github.com/ethereum/go-ethereum" ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" helpers "github.com/smartcontractkit/chainlink/core/scripts/common" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/values" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/ethereum/go-ethereum/accounts/abi/bind" gethCommon "github.com/ethereum/go-ethereum/common" @@ -26,6 +34,27 @@ import ( kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" ) +func sendTx(e helpers.Environment, to gethCommon.Address, data []byte) (*gethTypes.Receipt, gethCommon.Hash) { + nonce, err := e.Ec.PendingNonceAt(context.Background(), e.Owner.From) + helpers.PanicErr(err) + gasPrice, err := e.Ec.SuggestGasPrice(context.Background()) + helpers.PanicErr(err) + rawTx := gethTypes.NewTx(&gethTypes.LegacyTx{ + Nonce: nonce, + To: &to, + Data: data, + Value: big.NewInt(0), + Gas: 10_000_000, + GasPrice: gasPrice, + }) + signedTx, err := e.Owner.Signer(e.Owner.From, rawTx) + helpers.PanicErr(err) + err = e.Ec.SendTransaction(context.Background(), signedTx) + helpers.PanicErr(err) + return helpers.ConfirmTXMined(context.Background(), e.Ec, signedTx, + e.ChainID, "send tx", signedTx.Hash().String(), "to", to.String()), signedTx.Hash() +} + type CapabilityRegistryProvisioner struct { reg *kcr.CapabilitiesRegistry env helpers.Environment From c87a41e85660ff5c44dccdfb6e8f824d8dba85ef Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:24:25 -0700 Subject: [PATCH 28/33] Add missing env field to CR struct --- .../keystone/src/06_provision_capabilities_registry.go | 7 +++++-- core/scripts/keystone/src/88_capabilities_registry.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/scripts/keystone/src/06_provision_capabilities_registry.go b/core/scripts/keystone/src/06_provision_capabilities_registry.go index 9bc91be92f..73748f6dfa 100644 --- a/core/scripts/keystone/src/06_provision_capabilities_registry.go +++ b/core/scripts/keystone/src/06_provision_capabilities_registry.go @@ -6,6 +6,8 @@ import ( "fmt" "os" + "github.com/ethereum/go-ethereum/common" + helpers "github.com/smartcontractkit/chainlink/core/scripts/common" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" ) @@ -67,7 +69,7 @@ func (c *provisionCR) Run(args []string) { *chainID, *nodeList, ) - crProvisioner := NewCapabilityRegistryProvisioner(reg) + crProvisioner := NewCapabilityRegistryProvisioner(reg, env) // We're using the default capability set for now capSet := NewCapabilitySet() crProvisioner.AddCapabilities(ctx, capSet) @@ -99,7 +101,8 @@ func loadDON(publicKeys string, chainID int64, nodeList string) []peer { func getOrDeployCapabilitiesRegistry(ctx context.Context, artefactsDir string, env helpers.Environment) *kcr.CapabilitiesRegistry { contracts, err := LoadDeployedContracts(artefactsDir) if err != nil { - panic(err) + fmt.Println("Could not load deployed contracts, deploying new ones") + // panic(err) } if contracts.CapabilityRegistry.String() == (common.Address{}).String() { diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go index 57ce5c4206..2446de512e 100644 --- a/core/scripts/keystone/src/88_capabilities_registry.go +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -60,6 +60,9 @@ type CapabilityRegistryProvisioner struct { env helpers.Environment } +func NewCapabilityRegistryProvisioner(reg *kcr.CapabilitiesRegistry, env helpers.Environment) *CapabilityRegistryProvisioner { + return &CapabilityRegistryProvisioner{reg: reg, env: env} +} func extractRevertReason(errData string, a abi.ABI) (string, string, error) { data, err := hex.DecodeString(errData[2:]) From b37f41add6b1a19d199c5eb1b8ed6e4e79fdf398 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 19 Sep 2024 22:24:52 -0700 Subject: [PATCH 29/33] Fix CR deployment bugs --- .../keystone/src/88_capabilities_registry.go | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go index 2446de512e..129f7141b8 100644 --- a/core/scripts/keystone/src/88_capabilities_registry.go +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -120,11 +120,10 @@ func (c *CapabilityRegistryProvisioner) testCallContract(method string, args ... // AddCapabilities takes a capability set and provisions it in the registry. func (c *CapabilityRegistryProvisioner) AddCapabilities(ctx context.Context, capSet CapabilitySet) { - tx, err := c.reg.AddCapabilities(c.env.Owner, capSet.Capabilities()) - if err != nil { - log.Printf("failed to call AddCapabilities: %s", err) - } + c.testCallContract("addCapabilities", capSet.Capabilities()) + tx, err := c.reg.AddCapabilities(c.env.Owner, capSet.Capabilities()) + helpers.PanicErr(err) helpers.ConfirmTXMined(ctx, c.env.Ec, tx, c.env.ChainID) } @@ -138,8 +137,9 @@ func (c *CapabilityRegistryProvisioner) AddCapabilities(ctx context.Context, cap // The node operator is then added to the registry, and the registry will issue an ID for the node operator. // The ID is then used when adding nodes to the registry such that the registry knows which nodes belong to which // node operator. -func (c *CapabilityRegistryProvisioner) AddNodeOperator(ctx context.Context, nop NodeOperator) { +func (c *CapabilityRegistryProvisioner) AddNodeOperator(ctx context.Context, nop *NodeOperator) { nop.BindToRegistry(c.reg) + tx, err := c.reg.AddNodeOperators(c.env.Owner, []kcr.CapabilitiesRegistryNodeOperator{ { Admin: nop.Admin, @@ -163,11 +163,11 @@ func (c *CapabilityRegistryProvisioner) AddNodeOperator(ctx context.Context, nop // node operators to the same capability set. This is not yet implemented here. // // Note that the registry must already have the capability set added via `AddCapabilities`, you cannot -// add capabilites that the registry is not yet aware of. +// add capabilities that the registry is not yet aware of. // // Note that in terms of the provisioning process, this is not the last step. A capability is only active once // there is a DON servicing yet. This is done via `AddDON`. -func (c *CapabilityRegistryProvisioner) AddNodes(ctx context.Context, nop NodeOperator, capSet CapabilitySet) { +func (c *CapabilityRegistryProvisioner) AddNodes(ctx context.Context, nop *NodeOperator, capSet CapabilitySet) { params := []kcr.CapabilitiesRegistryNodeParams{} for _, peer := range nop.DON { node, innerErr := peerToNode(nop.id, peer) @@ -177,12 +177,14 @@ func (c *CapabilityRegistryProvisioner) AddNodes(ctx context.Context, nop NodeOp // Technically we could be more flexible here, // where we can have different capset assignment for each node - node.HashedCapabilityIds = capSet.CapabilityIDs() + node.HashedCapabilityIds = capSet.CapabilityIDs(c.reg) params = append(params, node) } + c.testCallContract("addNodes", params) tx, err := c.reg.AddNodes(c.env.Owner, params) + if err != nil { log.Printf("failed to AddNodes: %s", err) } @@ -210,13 +212,15 @@ func (c *CapabilityRegistryProvisioner) AddNodes(ctx context.Context, nop NodeOp // If you want to add a DON that services both capabilities and workflows, you should set both `acceptsWorkflows` and `isPublic` to true. // // Another important distinction is that DON can comprise of nodes from different node operators, but for now, we're keeping it simple and restricting it to a single node operator. We also hard code F to 1. -func (c *CapabilityRegistryProvisioner) AddDON(ctx context.Context, nop NodeOperator, capSet CapabilitySet, isPublic bool, acceptsWorkflows bool) { +func (c *CapabilityRegistryProvisioner) AddDON(ctx context.Context, nop *NodeOperator, capSet CapabilitySet, isPublic bool, acceptsWorkflows bool) { configs := capSet.Configs(c.reg) // Note: Technically we could be more flexible here, // where we can have multiple DONs with different capability configurations - // and have a non-hardcoded number for F - tx, err := c.reg.AddDON(c.env.Owner, nop.MustGetPeerIDs(), configs, isPublic, acceptsWorkflows, 1) + // and have a non-hardcoded number for F + var f uint8 = 1 + c.testCallContract("addDON", nop.MustGetPeerIDs(), configs, isPublic, acceptsWorkflows, f) + tx, err := c.reg.AddDON(c.env.Owner, nop.MustGetPeerIDs(), configs, isPublic, acceptsWorkflows, f) if err != nil { log.Printf("failed to AddDON: %s", err) } @@ -279,6 +283,7 @@ func (b *baseCapability) Capability() kcr.CapabilitiesRegistryCapability { type ConsensusCapability struct { baseCapability } + var _ CapabillityProvisioner = &ConsensusCapability{} func (c *ConsensusCapability) Config() kcr.CapabilitiesRegistryCapabilityConfiguration { @@ -291,7 +296,7 @@ func (c *ConsensusCapability) Config() kcr.CapabilitiesRegistryCapabilityConfigu return c.config(config) } -// NewOCR3V1ConsensusCapability returns a new ConsensusCapability for OCR3 +// NewOCR3V1ConsensusCapability returns a new ConsensusCapability for OCR3 func NewOCR3V1ConsensusCapability() *ConsensusCapability { return &ConsensusCapability{ baseCapability{ @@ -307,6 +312,7 @@ func NewOCR3V1ConsensusCapability() *ConsensusCapability { type TargetCapability struct { baseCapability } + var _ CapabillityProvisioner = &TargetCapability{} func (t *TargetCapability) Config() kcr.CapabilitiesRegistryCapabilityConfiguration { @@ -410,9 +416,10 @@ func (c *CapabilitySet) Capabilities() []kcr.CapabilitiesRegistryCapability { return definitions } -func (c *CapabilitySet) CapabilityIDs() [][32]byte { +func (c *CapabilitySet) CapabilityIDs(reg *kcr.CapabilitiesRegistry) [][32]byte { var ids [][32]byte for _, cap := range *c { + cap.BindToRegistry(reg) ids = append(ids, cap.GetHashedCID()) } @@ -450,8 +457,8 @@ type NodeOperator struct { id uint32 } -func NewNodeOperator(admin gethCommon.Address, name string, don []peer) NodeOperator { - return NodeOperator{ +func NewNodeOperator(admin gethCommon.Address, name string, don []peer) *NodeOperator { + return &NodeOperator{ Admin: admin, Name: name, DON: don, From 74af819b42de7a43f5bba1f257badd1bde759166 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:16:00 -0700 Subject: [PATCH 30/33] Fix trigger capability typo --- core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go | 4 ++-- .../__snapshots__/03_deploy_streams_trigger_cmd_test.snap | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go index ea5909b32f..baeaf6cc14 100644 --- a/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go +++ b/core/scripts/keystone/src/03_deploy_streams_trigger_cmd.go @@ -296,7 +296,7 @@ contractConfigTrackerPollInterval = "1s" [relayConfig] chainID = %d -enableTriggerCapabillity = true +enableTriggerCapability = true `, name, verifierAddress, feedID, chainID) return @@ -352,7 +352,7 @@ nativeFeedID = "0x%[9]x" serverURL = "wss://unknown" [relayConfig] -enableTriggerCapabillity = true +enableTriggerCapability = true chainID = "%[10]d" `, feedName, diff --git a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap index bc6caa0d69..f0c4d40736 100755 --- a/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap +++ b/core/scripts/keystone/src/__snapshots__/03_deploy_streams_trigger_cmd_test.snap @@ -34,7 +34,7 @@ nativeFeedID = "0x03000000000000000000000000000000000000000000000000000000000000 serverURL = "wss://unknown" [relayConfig] -enableTriggerCapabillity = true +enableTriggerCapability = true chainID = "123456" --- @@ -51,7 +51,7 @@ contractConfigTrackerPollInterval = "1s" [relayConfig] chainID = 123456 -enableTriggerCapabillity = true +enableTriggerCapability = true --- @@ -91,7 +91,7 @@ nativeFeedID = "0x03000000000000000000000000000000000000000000000000000000000000 serverURL = "wss://unknown" [relayConfig] -enableTriggerCapabillity = true +enableTriggerCapability = true chainID = "123456" --- From 2182cde63baa73015ae38ca6c473f9abd07b2d5e Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:17:37 -0700 Subject: [PATCH 31/33] Add external registry and capability p2p config gen --- .../src/03_gen_crib_cluster_overrides_cmd.go | 12 +++- .../03_gen_crib_cluster_overrides_cmd_test.go | 3 +- .../keystone/templates/crib-overrides.yaml | 59 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go index b731d66cb9..149e7fafe7 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd.go @@ -2,6 +2,7 @@ package src import ( "flag" + "fmt" "os" "path/filepath" "strings" @@ -47,16 +48,17 @@ func (g *generateCribClusterOverrides) Run(args []string) { deployedContracts, err := LoadDeployedContracts(*artefactsDir) helpers.PanicErr(err) - lines := generateCribConfig(*nodeList, *publicKeys, chainID, templatesDir, deployedContracts.ForwarderContract.Hex()) + lines := generateCribConfig(*nodeList, *publicKeys, chainID, templatesDir, deployedContracts.ForwarderContract.Hex(), deployedContracts.CapabilityRegistry.Hex()) cribOverridesStr := strings.Join(lines, "\n") err = os.WriteFile(filepath.Join(*outputPath, "crib-cluster-overrides.yaml"), []byte(cribOverridesStr), 0600) helpers.PanicErr(err) } -func generateCribConfig(nodeList string, pubKeysPath string, chainID *int64, templatesDir string, forwarderAddress string) []string { +func generateCribConfig(nodeList string, pubKeysPath string, chainID *int64, templatesDir string, forwarderAddress string, externalRegistryAddress string) []string { nca := downloadNodePubKeys(nodeList, *chainID, pubKeysPath) nodeAddresses := []string{} + capabilitiesBootstrapper := fmt.Sprintf("%s@%s:%s", nca[0].P2PPeerID, "app-node1", "6691") for _, node := range nca[1:] { nodeAddresses = append(nodeAddresses, node.EthAddress) @@ -64,7 +66,7 @@ func generateCribConfig(nodeList string, pubKeysPath string, chainID *int64, tem lines, err := readLines(filepath.Join(templatesDir, cribOverrideTemplate)) helpers.PanicErr(err) - lines = replaceCribPlaceholders(lines, forwarderAddress, nodeAddresses) + lines = replaceCribPlaceholders(lines, forwarderAddress, nodeAddresses, externalRegistryAddress, capabilitiesBootstrapper) return lines } @@ -72,6 +74,8 @@ func replaceCribPlaceholders( lines []string, forwarderAddress string, nodeFromAddresses []string, + externalRegistryAddress string, + capabilitiesBootstrapper string, ) (output []string) { for _, l := range lines { l = strings.Replace(l, "{{ forwarder_address }}", forwarderAddress, 1) @@ -79,6 +83,8 @@ func replaceCribPlaceholders( l = strings.Replace(l, "{{ node_3_address }}", nodeFromAddresses[1], 1) l = strings.Replace(l, "{{ node_4_address }}", nodeFromAddresses[2], 1) l = strings.Replace(l, "{{ node_5_address }}", nodeFromAddresses[3], 1) + l = strings.Replace(l, "{{ external_registry_address }}", externalRegistryAddress, 1) + l = strings.Replace(l, "{{ capabilities_bootstrapper }}", capabilitiesBootstrapper, 1) output = append(output, l) } diff --git a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go index ae42028261..5ab1ee4975 100644 --- a/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go +++ b/core/scripts/keystone/src/03_gen_crib_cluster_overrides_cmd_test.go @@ -11,9 +11,10 @@ func TestGenerateCribConfig(t *testing.T) { chainID := int64(1337) templatesDir := "../templates" forwarderAddress := "0x1234567890abcdef" + externalRegistryAddress := "0xabcdef1234567890" publicKeysPath := "./testdata/PublicKeys.json" - lines := generateCribConfig(defaultNodeList, publicKeysPath, &chainID, templatesDir, forwarderAddress) + lines := generateCribConfig(defaultNodeList, publicKeysPath, &chainID, templatesDir, forwarderAddress, externalRegistryAddress) snaps.MatchSnapshot(t, strings.Join(lines, "\n")) } diff --git a/core/scripts/keystone/templates/crib-overrides.yaml b/core/scripts/keystone/templates/crib-overrides.yaml index 758433bbc9..34355edd3c 100644 --- a/core/scripts/keystone/templates/crib-overrides.yaml +++ b/core/scripts/keystone/templates/crib-overrides.yaml @@ -7,6 +7,17 @@ helm: overridesToml: |- [[EVM]] ChainID = '1337' + + [Capabilities] + [Capabilities.Peering] + [Capabilities.Peering.V2] + Enabled = true + ListenAddresses = ['0.0.0.0:6691'] + + [Capabilities.ExternalRegistry] + Address = '{{ external_registry_address }}' + NetworkID = 'evm' + ChainID = '1337' node2: image: ${runtime.images.app} overridesToml: |- @@ -15,6 +26,18 @@ helm: [EVM.Workflow] FromAddress = '{{ node_2_address }}' ForwarderAddress = '{{ forwarder_address }}' + + [Capabilities] + [Capabilities.Peering] + [Capabilities.Peering.V2] + Enabled = true + DefaultBootstrappers = ['{{ capabilities_bootstrapper }}'] + ListenAddresses = ['0.0.0.0:6691'] + + [Capabilities.ExternalRegistry] + Address = '{{ external_registry_address }}' + NetworkID = 'evm' + ChainID = '1337' node3: image: ${runtime.images.app} overridesToml: |- @@ -23,6 +46,18 @@ helm: [EVM.Workflow] FromAddress = '{{ node_3_address }}' ForwarderAddress = '{{ forwarder_address }}' + + [Capabilities] + [Capabilities.Peering] + [Capabilities.Peering.V2] + Enabled = true + DefaultBootstrappers = ['{{ capabilities_bootstrapper }}'] + ListenAddresses = ['0.0.0.0:6691'] + + [Capabilities.ExternalRegistry] + Address = '{{ external_registry_address }}' + NetworkID = 'evm' + ChainID = '1337' node4: image: ${runtime.images.app} overridesToml: |- @@ -31,6 +66,18 @@ helm: [EVM.Workflow] FromAddress = '{{ node_4_address }}' ForwarderAddress = '{{ forwarder_address }}' + + [Capabilities] + [Capabilities.Peering] + [Capabilities.Peering.V2] + Enabled = true + DefaultBootstrappers = ['{{ capabilities_bootstrapper }}'] + ListenAddresses = ['0.0.0.0:6691'] + + [Capabilities.ExternalRegistry] + Address = '{{ external_registry_address }}' + NetworkID = 'evm' + ChainID = '1337' node5: image: ${runtime.images.app} overridesToml: |- @@ -39,3 +86,15 @@ helm: [EVM.Workflow] FromAddress = '{{ node_5_address }}' ForwarderAddress = '{{ forwarder_address }}' + + [Capabilities] + [Capabilities.Peering] + [Capabilities.Peering.V2] + Enabled = true + DefaultBootstrappers = ['{{ capabilities_bootstrapper }}'] + ListenAddresses = ['0.0.0.0:6691'] + + [Capabilities.ExternalRegistry] + Address = '{{ external_registry_address }}' + NetworkID = 'evm' + ChainID = '1337' From 011ddce7ba1ba014435627fef677425fdf7a1616 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:18:07 -0700 Subject: [PATCH 32/33] Add redial support for logging in --- core/scripts/keystone/src/99_app.go | 36 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/core/scripts/keystone/src/99_app.go b/core/scripts/keystone/src/99_app.go index afdbbdb97d..93b519603f 100644 --- a/core/scripts/keystone/src/99_app.go +++ b/core/scripts/keystone/src/99_app.go @@ -6,18 +6,30 @@ import ( "errors" "flag" "fmt" - "github.com/urfave/cli" "io" "reflect" "runtime" "strings" + "time" - "github.com/smartcontractkit/chainlink/v2/core/cmd" + "github.com/jpillora/backoff" + "github.com/urfave/cli" helpers "github.com/smartcontractkit/chainlink/core/scripts/common" + "github.com/smartcontractkit/chainlink/v2/core/cmd" clcmd "github.com/smartcontractkit/chainlink/v2/core/cmd" ) +// NewRedialBackoff is a standard backoff to use for redialling or reconnecting to +// unreachable network endpoints +func NewRedialBackoff() backoff.Backoff { + return backoff.Backoff{ + Min: 1 * time.Second, + Max: 15 * time.Second, + Jitter: true, + } +} + func newApp(n *node, writer io.Writer) (*clcmd.Shell, *cli.App) { client := &clcmd.Shell{ Renderer: clcmd.RendererJSON{Writer: writer}, @@ -62,8 +74,24 @@ func newNodeAPI(n *node) *nodeAPI { loginFs := flag.NewFlagSet("test", flag.ContinueOnError) loginFs.Bool("bypass-version-check", true, "") loginCtx := cli.NewContext(app, loginFs, nil) - err := methods.RemoteLogin(loginCtx) - helpers.PanicErr(err) + + redial := NewRedialBackoff() + + for { + err := methods.RemoteLogin(loginCtx) + if err == nil { + break + } + + fmt.Println("Error logging in:", err) + if strings.Contains(err.Error(), "invalid character '<' looking for beginning of value") { + fmt.Println("Likely a transient network error, retrying...") + } else { + helpers.PanicErr(err) + } + + time.Sleep(redial.Duration()) + } output.Reset() return api From baabe0b762ef70cadd5edf07827e32a36cc19217 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:18:40 -0700 Subject: [PATCH 33/33] Fix capability registration --- .../keystone/src/88_capabilities_registry.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/scripts/keystone/src/88_capabilities_registry.go b/core/scripts/keystone/src/88_capabilities_registry.go index 129f7141b8..5860aed5a4 100644 --- a/core/scripts/keystone/src/88_capabilities_registry.go +++ b/core/scripts/keystone/src/88_capabilities_registry.go @@ -234,10 +234,10 @@ func (c *CapabilityRegistryProvisioner) AddDON(ctx context.Context, nop *NodeOpe * */ const ( // Taken from https://github.com/smartcontractkit/chainlink/blob/29117850e9be1be1993dbf8f21cf13cbb6af9d24/core/capabilities/integration_tests/keystone_contracts_setup.go#L43 - CapabilityTypeTrigger = 0 - CapabilityTypeAction = 1 - CapabilityTypeConsensus = 2 - CapabilityTypeTarget = 3 + CapabilityTypeTrigger = uint8(0) + CapabilityTypeAction = uint8(1) + CapabilityTypeConsensus = uint8(2) + CapabilityTypeTarget = uint8(3) ) type CapabillityProvisioner interface { @@ -330,11 +330,11 @@ func (t *TargetCapability) Config() kcr.CapabilitiesRegistryCapabilityConfigurat return t.config(config) } -func NewEthereumSepoliaWriteTargetV1Capability() *TargetCapability { +func NewEthereumGethTestnetV1WriteCapability() *TargetCapability { return &TargetCapability{ baseCapability{ capability: kcr.CapabilitiesRegistryCapability{ - LabelledName: "write_ethereum-testnet_sepolia", + LabelledName: "write_geth-testnet", Version: "1.0.0", CapabilityType: CapabilityTypeTarget, }, @@ -396,10 +396,10 @@ type CapabilitySet []CapabillityProvisioner func NewCapabilitySet(capabilities ...CapabillityProvisioner) CapabilitySet { if len(capabilities) == 0 { - log.Println("No capabilities provided, using default capabillity set") + log.Println("No capabilities provided, using default capability set") return []CapabillityProvisioner{ NewStreamsTriggerV1Capability(), - NewEthereumSepoliaWriteTargetV1Capability(), + NewEthereumGethTestnetV1WriteCapability(), NewOCR3V1ConsensusCapability(), } }