From 5db63e713bae00b8f89d52667d1133f8be2a124f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 8 Nov 2024 23:52:00 +0900 Subject: [PATCH] deployment: Changes for keystone (#14837) * deployment: Support other chain types in CreateCCIPOCRSupportedChains * nix: Upgrade to postgres 15 * keystone: Migrate from CLO to JD * CLO compat * Allow setting labels on nodes * Rename function * Tag nodes with p2p_id for easy lookup * Lookup nodes according to p2p_id * Implement label & id filtering in the memory job client * Update the CLO job client as well * go mod tidy * Fix DeployCLO * Fix CLO job client test * add TODOs * fix up tests again --------- Co-authored-by: krehermann <16602512+krehermann@users.noreply.github.com> --- core/scripts/go.mod | 1 + deployment/environment/clo/env.go | 137 ----- .../environment/clo/offchain_client_impl.go | 84 ++- .../clo/offchain_client_impl_test.go | 32 ++ deployment/environment/devenv/don.go | 61 +- deployment/environment/memory/chain.go | 2 +- deployment/environment/memory/environment.go | 29 +- deployment/environment/memory/job_client.go | 168 +++++- deployment/environment/memory/node.go | 83 ++- deployment/environment/memory/node_test.go | 2 +- .../environment/web/sdk/client/client.go | 27 + .../web/sdk/internal/generated/generated.go | 91 +++ .../web/sdk/internal/genqlient.graphql | 16 +- deployment/go.mod | 2 +- deployment/keystone/changeset/deploy_ocr3.go | 6 +- .../changeset/internal/update_don_test.go | 116 ++-- deployment/keystone/changeset/types.go | 17 +- .../changeset/update_node_capabilities.go | 4 +- deployment/keystone/deploy.go | 118 +++- deployment/keystone/deploy_test.go | 268 ++++++++- deployment/keystone/types.go | 113 ++-- deployment/keystone/types_test.go | 540 +++++++++--------- shell.nix | 2 +- 23 files changed, 1233 insertions(+), 686 deletions(-) delete mode 100644 deployment/environment/clo/env.go diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 6fe84f15231..9d3b72711d7 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -50,6 +50,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect + github.com/AlekSi/pointer v1.1.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect github.com/CosmWasm/wasmd v0.40.1 // indirect diff --git a/deployment/environment/clo/env.go b/deployment/environment/clo/env.go deleted file mode 100644 index d1683ad4e1e..00000000000 --- a/deployment/environment/clo/env.go +++ /dev/null @@ -1,137 +0,0 @@ -package clo - -import ( - "strconv" - "testing" - - "github.com/test-go/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" - "github.com/smartcontractkit/chainlink/deployment/environment/memory" -) - -type DonEnvConfig struct { - DonName string - Chains map[uint64]deployment.Chain - Logger logger.Logger - Nops []*models.NodeOperator -} - -func NewDonEnv(t *testing.T, cfg DonEnvConfig) *deployment.Environment { - // no bootstraps in the don as far as capabilities registry is concerned - for _, nop := range cfg.Nops { - for _, node := range nop.Nodes { - for _, chain := range node.ChainConfigs { - if chain.Ocr2Config.IsBootstrap { - t.Fatalf("Don nodes should not be bootstraps nop %s node %s chain %s", nop.ID, node.ID, chain.Network.ChainID) - } - } - } - } - out := deployment.NewEnvironment( - cfg.DonName, - cfg.Logger, - deployment.NewMemoryAddressBook(), - cfg.Chains, - make([]string, 0), - NewJobClient(cfg.Logger, cfg.Nops), - ) - // assume that all the nodes in the provided input nops are part of the don - for _, nop := range cfg.Nops { - for _, node := range nop.Nodes { - out.NodeIDs = append(out.NodeIDs, node.ID) - } - } - - return out -} - -func NewDonEnvWithMemoryChains(t *testing.T, cfg DonEnvConfig, ignore func(*models.NodeChainConfig) bool) *deployment.Environment { - e := NewDonEnv(t, cfg) - // overwrite the chains with memory chains - chains := make(map[uint64]struct{}) - for _, nop := range cfg.Nops { - for _, node := range nop.Nodes { - for _, chain := range node.ChainConfigs { - if ignore(chain) { - continue - } - id, err := strconv.ParseUint(chain.Network.ChainID, 10, 64) - require.NoError(t, err, "failed to parse chain id to uint64") - chains[id] = struct{}{} - } - } - } - var cs []uint64 - for c := range chains { - cs = append(cs, c) - } - memoryChains := memory.NewMemoryChainsWithChainIDs(t, cs) - e.Chains = memoryChains - return e -} - -// MultiDonEnvironment is a single logical deployment environment (like dev, testnet, prod,...). -// It represents the idea that different nodesets host different capabilities. -// Each element in the DonEnv is a logical set of nodes that host the same capabilities. -// This model allows us to reuse the existing Environment abstraction while supporting multiple nodesets at -// expense of slightly abusing the original abstraction. Specifically, the abuse is that -// each Environment in the DonToEnv map is a subset of the target deployment environment. -// One element cannot represent dev and other testnet for example. -type MultiDonEnvironment struct { - donToEnv map[string]*deployment.Environment - Logger logger.Logger - // hacky but temporary to transition to Environment abstraction. set by New - Chains map[uint64]deployment.Chain -} - -func (mde MultiDonEnvironment) Flatten(name string) *deployment.Environment { - // TODO: KS-460 integrate with the clo offchain client impl - // may need to extend the Environment abstraction use maps rather than slices for Nodes - // somehow we need to capture the fact that each nodes belong to nodesets which have different capabilities - // purposely nil to catch misuse until we do that work - return deployment.NewEnvironment( - name, - mde.Logger, - deployment.NewMemoryAddressBook(), - mde.Chains, - nil, - nil, - ) -} - -func newMultiDonEnvironment(logger logger.Logger, donToEnv map[string]*deployment.Environment) *MultiDonEnvironment { - chains := make(map[uint64]deployment.Chain) - for _, env := range donToEnv { - for sel, chain := range env.Chains { - if _, exists := chains[sel]; !exists { - chains[sel] = chain - } - } - } - return &MultiDonEnvironment{ - donToEnv: donToEnv, - Logger: logger, - Chains: chains, - } -} - -func NewTestEnv(t *testing.T, lggr logger.Logger, dons map[string]*deployment.Environment) *MultiDonEnvironment { - for _, don := range dons { - //don := don - seen := make(map[uint64]deployment.Chain) - // ensure that generated chains are the same for all environments. this ensures that he in memory representation - // points to a common object for all dons given the same selector. - for sel, chain := range don.Chains { - c, exists := seen[sel] - if exists { - don.Chains[sel] = c - } else { - seen[sel] = chain - } - } - } - return newMultiDonEnvironment(lggr, dons) -} diff --git a/deployment/environment/clo/offchain_client_impl.go b/deployment/environment/clo/offchain_client_impl.go index e670663b925..f8962e1440d 100644 --- a/deployment/environment/clo/offchain_client_impl.go +++ b/deployment/environment/clo/offchain_client_impl.go @@ -2,14 +2,19 @@ package clo import ( "context" + "fmt" + "slices" + "strings" "go.uber.org/zap" "google.golang.org/grpc" + "github.com/AlekSi/pointer" "github.com/smartcontractkit/chainlink-common/pkg/logger" csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa" jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes" "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" ) @@ -60,39 +65,64 @@ func (j JobClient) GetNode(ctx context.Context, in *nodev1.GetNodeRequest, opts } func (j JobClient) ListNodes(ctx context.Context, in *nodev1.ListNodesRequest, opts ...grpc.CallOption) (*nodev1.ListNodesResponse, error) { - //TODO CCIP-3108 - var fiterIds map[string]struct{} - include := func(id string) bool { - if in.Filter == nil || len(in.Filter.Ids) == 0 { + include := func(node *nodev1.Node) bool { + if in.Filter == nil { return true } - // lazy init - if len(fiterIds) == 0 { - for _, id := range in.Filter.Ids { - fiterIds[id] = struct{}{} + if len(in.Filter.Ids) > 0 { + idx := slices.IndexFunc(in.Filter.Ids, func(id string) bool { + return node.Id == id + }) + if idx < 0 { + return false } } - _, ok := fiterIds[id] - return ok + for _, selector := range in.Filter.Selectors { + idx := slices.IndexFunc(node.Labels, func(label *ptypes.Label) bool { + return label.Key == selector.Key + }) + if idx < 0 { + return false + } + label := node.Labels[idx] + + switch selector.Op { + case ptypes.SelectorOp_IN: + values := strings.Split(*selector.Value, ",") + found := slices.Contains(values, *label.Value) + if !found { + return false + } + default: + panic("unimplemented selector") + } + } + return true } var nodes []*nodev1.Node for _, nop := range j.NodeOperators { for _, n := range nop.Nodes { - if include(n.ID) { - nodes = append(nodes, &nodev1.Node{ - Id: n.ID, - Name: n.Name, - PublicKey: *n.PublicKey, // is this the correct val? - IsEnabled: n.Enabled, - IsConnected: n.Connected, - }) + node := &nodev1.Node{ + Id: n.ID, + Name: n.Name, + PublicKey: *n.PublicKey, + IsEnabled: n.Enabled, + IsConnected: n.Connected, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString(n.ID), // here n.ID is also peer ID + }, + }, + } + if include(node) { + nodes = append(nodes, node) } } } return &nodev1.ListNodesResponse{ Nodes: nodes, }, nil - } func (j JobClient) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNodeChainConfigsRequest, opts ...grpc.CallOption) (*nodev1.ListNodeChainConfigsResponse, error) { @@ -184,10 +214,24 @@ func cloNodeToChainConfigs(n *models.Node) []*nodev1.ChainConfig { } func cloChainCfgToJDChainCfg(ccfg *models.NodeChainConfig) *nodev1.ChainConfig { + var ctype nodev1.ChainType + switch ccfg.Network.ChainType { + case models.ChainTypeEvm: + ctype = nodev1.ChainType_CHAIN_TYPE_EVM + case models.ChainTypeSolana: + ctype = nodev1.ChainType_CHAIN_TYPE_SOLANA + case models.ChainTypeStarknet: + ctype = nodev1.ChainType_CHAIN_TYPE_STARKNET + case models.ChainTypeAptos: + ctype = nodev1.ChainType_CHAIN_TYPE_APTOS + default: + panic(fmt.Sprintf("Unsupported chain family %v", ccfg.Network.ChainType)) + } + return &nodev1.ChainConfig{ Chain: &nodev1.Chain{ Id: ccfg.Network.ChainID, - Type: nodev1.ChainType_CHAIN_TYPE_EVM, // TODO: write conversion func from clo to jd tyes + Type: ctype, }, AccountAddress: ccfg.AccountAddress, AdminAddress: ccfg.AdminAddress, diff --git a/deployment/environment/clo/offchain_client_impl_test.go b/deployment/environment/clo/offchain_client_impl_test.go index 3c9277d9fb0..bc6f30a7db1 100644 --- a/deployment/environment/clo/offchain_client_impl_test.go +++ b/deployment/environment/clo/offchain_client_impl_test.go @@ -6,10 +6,12 @@ import ( "reflect" "testing" + "github.com/AlekSi/pointer" "github.com/test-go/testify/require" "google.golang.org/grpc" nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes" "github.com/smartcontractkit/chainlink/deployment/environment/clo" "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -135,6 +137,12 @@ func TestJobClient_ListNodes(t *testing.T) { Name: "Chainlink Sepolia Prod Keystone One 9", PublicKey: "412dc6fe48ea4e34baaa77da2e3b032d39b938597b6f3d61fe7ed183a827a431", IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString("780"), + }, + }, }, }, }, @@ -155,12 +163,24 @@ func TestJobClient_ListNodes(t *testing.T) { Name: "Chainlink Sepolia Prod Keystone One 9", PublicKey: "412dc6fe48ea4e34baaa77da2e3b032d39b938597b6f3d61fe7ed183a827a431", IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString("780"), + }, + }, }, { Id: "781", Name: "Chainlink Sepolia Prod Keystone One 8", PublicKey: "1141dd1e46797ced9b0fbad49115f18507f6f6e6e3cc86e7e5ba169e58645adc", IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString("781"), + }, + }, }, }, }, @@ -181,12 +201,24 @@ func TestJobClient_ListNodes(t *testing.T) { Name: "Chainlink Sepolia Prod Keystone One 999", PublicKey: "9991dd1e46797ced9b0fbad49115f18507f6f6e6e3cc86e7e5ba169e58999999", IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString("999"), + }, + }, }, { Id: "1000", Name: "Chainlink Sepolia Prod Keystone One 1000", PublicKey: "1000101e46797ced9b0fbad49115f18507f6f6e6e3cc86e7e5ba169e58641000", IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString("1000"), + }, + }, }, }, }, diff --git a/deployment/environment/devenv/don.go b/deployment/environment/devenv/don.go index c14216f3894..f9b449e208b 100644 --- a/deployment/environment/devenv/don.go +++ b/deployment/environment/devenv/don.go @@ -34,6 +34,7 @@ type NodeInfo struct { Name string // name of the node, used to identify the node, helpful in logs AdminAddr string // admin address to send payments to, applicable only for non-bootstrap nodes MultiAddr string // multi address denoting node's FQN (needed for deriving P2PBootstrappers in OCR), applicable only for bootstrap nodes + Labels map[string]string // labels to use when registering the node with job distributor } type DON struct { @@ -104,6 +105,12 @@ func NewRegisteredDON(ctx context.Context, nodeInfo []NodeInfo, jd JobDistributo return nil, fmt.Errorf("failed to create node %d: %w", i, err) } // node Labels so that it's easier to query them + for key, value := range info.Labels { + node.labels = append(node.labels, &ptypes.Label{ + Key: key, + Value: pointer.ToString(value), + }) + } if info.IsBootstrap { // create multi address for OCR2, applicable only for bootstrap nodes if info.MultiAddr == "" { @@ -181,17 +188,35 @@ type JDChainConfigInput struct { func (n *Node) CreateCCIPOCRSupportedChains(ctx context.Context, chains []JDChainConfigInput, jd JobDistributor) error { for i, chain := range chains { chainId := strconv.FormatUint(chain.ChainID, 10) - accountAddr, err := n.gqlClient.FetchAccountAddress(ctx, chainId) - if err != nil { - return fmt.Errorf("failed to fetch account address for node %s: %w", n.Name, err) - } - if accountAddr == nil { - return fmt.Errorf("no account address found for node %s", n.Name) - } - if n.AccountAddr == nil { - n.AccountAddr = make(map[uint64]string) + var account string + switch chain.ChainType { + case "EVM": + accountAddr, err := n.gqlClient.FetchAccountAddress(ctx, chainId) + if err != nil { + return fmt.Errorf("failed to fetch account address for node %s: %w", n.Name, err) + } + if accountAddr == nil { + return fmt.Errorf("no account address found for node %s", n.Name) + } + if n.AccountAddr == nil { + n.AccountAddr = make(map[uint64]string) + } + n.AccountAddr[chain.ChainID] = *accountAddr + account = *accountAddr + case "APTOS", "SOLANA": + accounts, err := n.gqlClient.FetchKeys(ctx, chain.ChainType) + if err != nil { + return fmt.Errorf("failed to fetch account address for node %s: %w", n.Name, err) + } + if len(accounts) == 0 { + return fmt.Errorf("no account address found for node %s", n.Name) + } + + account = accounts[0] + default: + return fmt.Errorf("unsupported chainType %v", chain.ChainType) } - n.AccountAddr[chain.ChainID] = *accountAddr + peerID, err := n.gqlClient.FetchP2PPeerID(ctx) if err != nil { return fmt.Errorf("failed to fetch peer id for node %s: %w", n.Name, err) @@ -221,7 +246,7 @@ func (n *Node) CreateCCIPOCRSupportedChains(ctx context.Context, chains []JDChai JobDistributorID: n.JDId, ChainID: chainId, ChainType: chain.ChainType, - AccountAddr: pointer.GetString(accountAddr), + AccountAddr: account, AdminAddr: n.adminAddr, Ocr2Enabled: true, Ocr2IsBootstrap: isBootstrap, @@ -291,6 +316,20 @@ func (n *Node) RegisterNodeToJobDistributor(ctx context.Context, jd JobDistribut return fmt.Errorf("no csa key found for node %s", n.Name) } csaKey := strings.TrimPrefix(*csaKeyRes, "csa_") + + // tag nodes with p2p_id for easy lookup + peerID, err := n.gqlClient.FetchP2PPeerID(ctx) + if err != nil { + return fmt.Errorf("failed to fetch peer id for node %s: %w", n.Name, err) + } + if peerID == nil { + return fmt.Errorf("no peer id found for node %s", n.Name) + } + n.labels = append(n.labels, &ptypes.Label{ + Key: "p2p_id", + Value: pointer.ToString(*peerID), + }) + // register the node in the job distributor registerResponse, err := jd.RegisterNode(ctx, &nodev1.RegisterNodeRequest{ PublicKey: csaKey, diff --git a/deployment/environment/memory/chain.go b/deployment/environment/memory/chain.go index 0f3badc7dca..d66200685ea 100644 --- a/deployment/environment/memory/chain.go +++ b/deployment/environment/memory/chain.go @@ -85,7 +85,7 @@ func GenerateChainsWithIds(t *testing.T, chainIDs []uint64) map[uint64]EVMChain owner, err := bind.NewKeyedTransactorWithChainID(key, big.NewInt(1337)) require.NoError(t, err) backend := backends.NewSimulatedBackend(core.GenesisAlloc{ - owner.From: {Balance: big.NewInt(0).Mul(big.NewInt(100), big.NewInt(params.Ether))}}, 10000000) + owner.From: {Balance: big.NewInt(0).Mul(big.NewInt(700000), big.NewInt(params.Ether))}}, 50000000) tweakChainTimestamp(t, backend, time.Hour*8) chains[chainID] = EVMChain{ Backend: backend, diff --git a/deployment/environment/memory/environment.go b/deployment/environment/memory/environment.go index 22733571038..ae2ba54c015 100644 --- a/deployment/environment/memory/environment.go +++ b/deployment/environment/memory/environment.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/core/types" "github.com/hashicorp/consul/sdk/freeport" "github.com/stretchr/testify/require" @@ -29,6 +29,18 @@ type MemoryEnvironmentConfig struct { RegistryConfig deployment.CapabilityRegistryConfig } +// For placeholders like aptos +func NewMemoryChain(t *testing.T, selector uint64) deployment.Chain { + return deployment.Chain{ + Selector: selector, + Client: nil, + DeployerKey: &bind.TransactOpts{}, + Confirm: func(tx *types.Transaction) (uint64, error) { + return 0, nil + }, + } +} + // Needed for environment variables on the node which point to prexisitng addresses. // i.e. CapReg. func NewMemoryChains(t *testing.T, numChains int) map[uint64]deployment.Chain { @@ -78,30 +90,19 @@ func generateMemoryChain(t *testing.T, inputs map[uint64]EVMChain) map[uint64]de } func NewNodes(t *testing.T, logLevel zapcore.Level, chains map[uint64]deployment.Chain, numNodes, numBootstraps int, registryConfig deployment.CapabilityRegistryConfig) map[string]Node { - mchains := make(map[uint64]EVMChain) - for _, chain := range chains { - evmChainID, err := chainsel.ChainIdFromSelector(chain.Selector) - if err != nil { - t.Fatal(err) - } - mchains[evmChainID] = EVMChain{ - Backend: chain.Client.(*backends.SimulatedBackend), - DeployerKey: chain.DeployerKey, - } - } nodesByPeerID := make(map[string]Node) ports := freeport.GetN(t, numBootstraps+numNodes) // bootstrap nodes must be separate nodes from plugin nodes, // since we won't run a bootstrapper and a plugin oracle on the same // chainlink node in production. for i := 0; i < numBootstraps; i++ { - node := NewNode(t, ports[i], mchains, logLevel, true /* bootstrap */, registryConfig) + node := NewNode(t, ports[i], chains, logLevel, true /* bootstrap */, registryConfig) nodesByPeerID[node.Keys.PeerID.String()] = *node // Note in real env, this ID is allocated by JD. } for i := 0; i < numNodes; i++ { // grab port offset by numBootstraps, since above loop also takes some ports. - node := NewNode(t, ports[numBootstraps+i], mchains, logLevel, false /* bootstrap */, registryConfig) + node := NewNode(t, ports[numBootstraps+i], chains, logLevel, false /* bootstrap */, registryConfig) nodesByPeerID[node.Keys.PeerID.String()] = *node // Note in real env, this ID is allocated by JD. } diff --git a/deployment/environment/memory/job_client.go b/deployment/environment/memory/job_client.go index d572f5f92f5..f243ef63818 100644 --- a/deployment/environment/memory/job_client.go +++ b/deployment/environment/memory/job_client.go @@ -4,15 +4,22 @@ import ( "context" "errors" "fmt" + "slices" "strconv" + "strings" + "github.com/AlekSi/pointer" "github.com/ethereum/go-ethereum/common" "google.golang.org/grpc" + chainsel "github.com/smartcontractkit/chain-selectors" + csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa" jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip/validate" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" ) type JobClient struct { @@ -62,7 +69,7 @@ func (j JobClient) GetNode(ctx context.Context, in *nodev1.GetNodeRequest, opts return &nodev1.GetNodeResponse{ Node: &nodev1.Node{ Id: in.Id, - PublicKey: n.Keys.OCRKeyBundle.ID(), // is this the correct val? + PublicKey: n.Keys.CSA.PublicKeyString(), IsEnabled: true, IsConnected: true, }, @@ -71,35 +78,61 @@ func (j JobClient) GetNode(ctx context.Context, in *nodev1.GetNodeRequest, opts func (j JobClient) ListNodes(ctx context.Context, in *nodev1.ListNodesRequest, opts ...grpc.CallOption) (*nodev1.ListNodesResponse, error) { //TODO CCIP-3108 - var fiterIds map[string]struct{} - include := func(id string) bool { - if in.Filter == nil || len(in.Filter.Ids) == 0 { + include := func(node *nodev1.Node) bool { + if in.Filter == nil { return true } - // lazy init - if len(fiterIds) == 0 { - for _, id := range in.Filter.Ids { - fiterIds[id] = struct{}{} + if len(in.Filter.Ids) > 0 { + idx := slices.IndexFunc(in.Filter.Ids, func(id string) bool { + return node.Id == id + }) + if idx < 0 { + return false + } + } + for _, selector := range in.Filter.Selectors { + idx := slices.IndexFunc(node.Labels, func(label *ptypes.Label) bool { + return label.Key == selector.Key + }) + if idx < 0 { + return false + } + label := node.Labels[idx] + + switch selector.Op { + case ptypes.SelectorOp_IN: + values := strings.Split(*selector.Value, ",") + found := slices.Contains(values, *label.Value) + if !found { + return false + } + default: + panic("unimplemented selector") } } - _, ok := fiterIds[id] - return ok + return true } var nodes []*nodev1.Node for id, n := range j.Nodes { - if include(id) { - nodes = append(nodes, &nodev1.Node{ - Id: id, - PublicKey: n.Keys.OCRKeyBundle.ID(), // is this the correct val? - IsEnabled: true, - IsConnected: true, - }) + node := &nodev1.Node{ + Id: id, + PublicKey: n.Keys.CSA.ID(), + IsEnabled: true, + IsConnected: true, + Labels: []*ptypes.Label{ + { + Key: "p2p_id", + Value: pointer.ToString(n.Keys.PeerID.String()), + }, + }, + } + if include(node) { + nodes = append(nodes, node) } } return &nodev1.ListNodesResponse{ Nodes: nodes, }, nil - } func (j JobClient) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNodeChainConfigsRequest, opts ...grpc.CallOption) (*nodev1.ListNodeChainConfigsResponse, error) { @@ -113,8 +146,17 @@ func (j JobClient) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNode if !ok { return nil, fmt.Errorf("node id not found: %s", in.Filter.NodeIds[0]) } - offpk := n.Keys.OCRKeyBundle.OffchainPublicKey() - cpk := n.Keys.OCRKeyBundle.ConfigEncryptionPublicKey() + evmBundle := n.Keys.OCRKeyBundles[chaintype.EVM] + offpk := evmBundle.OffchainPublicKey() + cpk := evmBundle.ConfigEncryptionPublicKey() + + evmKeyBundle := &nodev1.OCR2Config_OCRKeyBundle{ + BundleId: evmBundle.ID(), + ConfigPublicKey: common.Bytes2Hex(cpk[:]), + OffchainPublicKey: common.Bytes2Hex(offpk[:]), + OnchainSigningAddress: evmBundle.OnChainPublicKey(), + } + var chainConfigs []*nodev1.ChainConfig for evmChainID, transmitter := range n.Keys.TransmittersByEVMChainID { chainConfigs = append(chainConfigs, &nodev1.ChainConfig{ @@ -123,7 +165,7 @@ func (j JobClient) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNode Type: nodev1.ChainType_CHAIN_TYPE_EVM, }, AccountAddress: transmitter.String(), - AdminAddress: "", + AdminAddress: transmitter.String(), // TODO: custom address Ocr1Config: nil, Ocr2Config: &nodev1.OCR2Config{ Enabled: true, @@ -131,19 +173,91 @@ func (j JobClient) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNode P2PKeyBundle: &nodev1.OCR2Config_P2PKeyBundle{ PeerId: n.Keys.PeerID.String(), }, - OcrKeyBundle: &nodev1.OCR2Config_OCRKeyBundle{ - BundleId: n.Keys.OCRKeyBundle.ID(), - ConfigPublicKey: common.Bytes2Hex(cpk[:]), - OffchainPublicKey: common.Bytes2Hex(offpk[:]), - OnchainSigningAddress: n.Keys.OCRKeyBundle.OnChainPublicKey(), - }, + OcrKeyBundle: evmKeyBundle, Multiaddr: n.Addr.String(), Plugins: nil, ForwarderAddress: ptr(""), }, }) } + for _, selector := range n.Chains { + family, err := chainsel.GetSelectorFamily(selector) + if err != nil { + return nil, err + } + chainID, err := chainsel.ChainIdFromSelector(selector) + if err != nil { + return nil, err + } + + if family == chainsel.FamilyEVM { + // already handled above + continue + } + + var ocrtype chaintype.ChainType + switch family { + case chainsel.FamilyEVM: + ocrtype = chaintype.EVM + case chainsel.FamilySolana: + ocrtype = chaintype.Solana + case chainsel.FamilyStarknet: + ocrtype = chaintype.StarkNet + case chainsel.FamilyCosmos: + ocrtype = chaintype.Cosmos + case chainsel.FamilyAptos: + ocrtype = chaintype.Aptos + default: + panic(fmt.Sprintf("Unsupported chain family %v", family)) + } + + bundle := n.Keys.OCRKeyBundles[ocrtype] + + offpk := bundle.OffchainPublicKey() + cpk := bundle.ConfigEncryptionPublicKey() + + keyBundle := &nodev1.OCR2Config_OCRKeyBundle{ + BundleId: bundle.ID(), + ConfigPublicKey: common.Bytes2Hex(cpk[:]), + OffchainPublicKey: common.Bytes2Hex(offpk[:]), + OnchainSigningAddress: bundle.OnChainPublicKey(), + } + + var ctype nodev1.ChainType + switch family { + case chainsel.FamilyEVM: + ctype = nodev1.ChainType_CHAIN_TYPE_EVM + case chainsel.FamilySolana: + ctype = nodev1.ChainType_CHAIN_TYPE_SOLANA + case chainsel.FamilyStarknet: + ctype = nodev1.ChainType_CHAIN_TYPE_STARKNET + case chainsel.FamilyAptos: + ctype = nodev1.ChainType_CHAIN_TYPE_APTOS + default: + panic(fmt.Sprintf("Unsupported chain family %v", family)) + } + chainConfigs = append(chainConfigs, &nodev1.ChainConfig{ + Chain: &nodev1.Chain{ + Id: strconv.Itoa(int(chainID)), + Type: ctype, + }, + AccountAddress: "", // TODO: support AccountAddress + AdminAddress: "", + Ocr1Config: nil, + Ocr2Config: &nodev1.OCR2Config{ + Enabled: true, + IsBootstrap: n.IsBoostrap, + P2PKeyBundle: &nodev1.OCR2Config_P2PKeyBundle{ + PeerId: n.Keys.PeerID.String(), + }, + OcrKeyBundle: keyBundle, + Multiaddr: n.Addr.String(), + Plugins: nil, + ForwarderAddress: ptr(""), + }, + }) + } // TODO: I think we can pull it from the feeds manager. return &nodev1.ListNodeChainConfigsResponse{ ChainConfigs: chainConfigs, diff --git a/deployment/environment/memory/node.go b/deployment/environment/memory/node.go index a2a690cbae5..133c70b8b29 100644 --- a/deployment/environment/memory/node.go +++ b/deployment/environment/memory/node.go @@ -10,11 +10,13 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/loop" @@ -35,6 +37,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/csakey" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" "github.com/smartcontractkit/chainlink/v2/core/services/relay" @@ -46,6 +49,7 @@ import ( type Node struct { App chainlink.Application // Transmitter key/OCR keys for this node + Chains []uint64 // chain selectors Keys Keys Addr net.TCPAddr IsBoostrap bool @@ -68,11 +72,23 @@ func (n Node) ReplayLogs(chains map[uint64]uint64) error { func NewNode( t *testing.T, port int, // Port for the P2P V2 listener. - chains map[uint64]EVMChain, + chains map[uint64]deployment.Chain, logLevel zapcore.Level, bootstrap bool, registryConfig deployment.CapabilityRegistryConfig, ) *Node { + evmchains := make(map[uint64]EVMChain) + for _, chain := range chains { + evmChainID, err := chainsel.ChainIdFromSelector(chain.Selector) + if err != nil { + t.Fatal(err) + } + evmchains[evmChainID] = EVMChain{ + Backend: chain.Client.(*backends.SimulatedBackend), + DeployerKey: chain.DeployerKey, + } + } + // Do not want to load fixtures as they contain a dummy chainID. // Create database and initial configuration. cfg, db := heavyweight.FullTestDBNoFixturesV2(t, func(c *chainlink.Config, s *chainlink.Secrets) { @@ -102,7 +118,7 @@ func NewNode( c.Log.Level = ptr(configv2.LogLevel(logLevel)) var chainConfigs v2toml.EVMConfigs - for chainID := range chains { + for chainID := range evmchains { chainConfigs = append(chainConfigs, createConfigV2Chain(chainID)) } c.EVM = chainConfigs @@ -114,7 +130,7 @@ func NewNode( // Create clients for the core node backed by sim. clients := make(map[uint64]client.Client) - for chainID, chain := range chains { + for chainID, chain := range evmchains { clients[chainID] = client.NewSimulatedBackendClient(t, chain.Backend, big.NewInt(int64(chainID))) } @@ -178,6 +194,7 @@ func NewNode( return &Node{ App: app, + Chains: maps.Keys(chains), Keys: keys, Addr: net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}, IsBoostrap: bootstrap, @@ -186,50 +203,84 @@ func NewNode( type Keys struct { PeerID p2pkey.PeerID + CSA csakey.KeyV2 TransmittersByEVMChainID map[uint64]common.Address - OCRKeyBundle ocr2key.KeyBundle + OCRKeyBundles map[chaintype.ChainType]ocr2key.KeyBundle } func CreateKeys(t *testing.T, - app chainlink.Application, chains map[uint64]EVMChain) Keys { + app chainlink.Application, chains map[uint64]deployment.Chain) Keys { ctx := tests.Context(t) require.NoError(t, app.GetKeyStore().Unlock(ctx, "password")) _, err := app.GetKeyStore().P2P().Create(ctx) require.NoError(t, err) + csaKey, err := app.GetKeyStore().CSA().Create(ctx) + require.NoError(t, err) + p2pIDs, err := app.GetKeyStore().P2P().GetAll() require.NoError(t, err) require.Len(t, p2pIDs, 1) peerID := p2pIDs[0].PeerID() // create a transmitter for each chain transmitters := make(map[uint64]common.Address) - for chainID, chain := range chains { - cid := big.NewInt(int64(chainID)) + keybundles := make(map[chaintype.ChainType]ocr2key.KeyBundle) + for _, chain := range chains { + family, err := chainsel.GetSelectorFamily(chain.Selector) + require.NoError(t, err) + + var ctype chaintype.ChainType + switch family { + case chainsel.FamilyEVM: + ctype = chaintype.EVM + case chainsel.FamilySolana: + ctype = chaintype.Solana + case chainsel.FamilyStarknet: + ctype = chaintype.StarkNet + case chainsel.FamilyCosmos: + ctype = chaintype.Cosmos + case chainsel.FamilyAptos: + ctype = chaintype.Aptos + default: + panic(fmt.Sprintf("Unsupported chain family %v", family)) + } + + keybundle, err := app.GetKeyStore().OCR2().Create(ctx, ctype) + require.NoError(t, err) + keybundles[ctype] = keybundle + + if family != chainsel.FamilyEVM { + // TODO: only support EVM transmission keys for now + continue + } + + evmChainID, err := chainsel.ChainIdFromSelector(chain.Selector) + require.NoError(t, err) + + cid := big.NewInt(int64(evmChainID)) addrs, err2 := app.GetKeyStore().Eth().EnabledAddressesForChain(ctx, cid) require.NoError(t, err2) if len(addrs) == 1 { // just fund the address - fundAddress(t, chain.DeployerKey, addrs[0], assets.Ether(10).ToInt(), chain.Backend) - transmitters[chainID] = addrs[0] + transmitters[evmChainID] = addrs[0] } else { // create key and fund it _, err3 := app.GetKeyStore().Eth().Create(ctx, cid) - require.NoError(t, err3, "failed to create key for chain", chainID) + require.NoError(t, err3, "failed to create key for chain", evmChainID) sendingKeys, err3 := app.GetKeyStore().Eth().EnabledAddressesForChain(ctx, cid) require.NoError(t, err3) require.Len(t, sendingKeys, 1) - fundAddress(t, chain.DeployerKey, sendingKeys[0], assets.Ether(10).ToInt(), chain.Backend) - transmitters[chainID] = sendingKeys[0] + transmitters[evmChainID] = sendingKeys[0] } + backend := chain.Client.(*backends.SimulatedBackend) + fundAddress(t, chain.DeployerKey, transmitters[evmChainID], assets.Ether(1000).ToInt(), backend) } - require.Len(t, transmitters, len(chains)) - keybundle, err := app.GetKeyStore().OCR2().Create(ctx, chaintype.EVM) - require.NoError(t, err) return Keys{ PeerID: peerID, + CSA: csaKey, TransmittersByEVMChainID: transmitters, - OCRKeyBundle: keybundle, + OCRKeyBundles: keybundles, } } diff --git a/deployment/environment/memory/node_test.go b/deployment/environment/memory/node_test.go index 9142f48bbfe..7cbcb66d04a 100644 --- a/deployment/environment/memory/node_test.go +++ b/deployment/environment/memory/node_test.go @@ -12,7 +12,7 @@ import ( ) func TestNode(t *testing.T) { - chains := GenerateChains(t, 3) + chains := NewMemoryChains(t, 3) ports := freeport.GetN(t, 1) node := NewNode(t, ports[0], chains, zapcore.DebugLevel, false, deployment.CapabilityRegistryConfig{}) // We expect 3 transmitter keys diff --git a/deployment/environment/web/sdk/client/client.go b/deployment/environment/web/sdk/client/client.go index b22f52f3af4..162c6159004 100644 --- a/deployment/environment/web/sdk/client/client.go +++ b/deployment/environment/web/sdk/client/client.go @@ -18,6 +18,7 @@ type Client interface { FetchCSAPublicKey(ctx context.Context) (*string, error) FetchP2PPeerID(ctx context.Context) (*string, error) FetchAccountAddress(ctx context.Context, chainID string) (*string, error) + FetchKeys(ctx context.Context, chainType string) ([]string, error) FetchOCR2KeyBundleID(ctx context.Context, chainType string) (string, error) GetJob(ctx context.Context, id string) (*generated.GetJobResponse, error) ListJobs(ctx context.Context, offset, limit int) (*generated.ListJobsResponse, error) @@ -127,6 +128,32 @@ func (c *client) FetchAccountAddress(ctx context.Context, chainID string) (*stri return nil, fmt.Errorf("no account found for chain %s", chainID) } +func (c *client) FetchKeys(ctx context.Context, chainType string) ([]string, error) { + keys, err := generated.FetchKeys(ctx, c.gqlClient) + if err != nil { + return nil, err + } + if keys == nil { + return nil, fmt.Errorf("no accounts found") + } + switch generated.OCR2ChainType(chainType) { + case generated.OCR2ChainTypeAptos: + var accounts []string + for _, key := range keys.AptosKeys.GetResults() { + accounts = append(accounts, key.Account) + } + return accounts, nil + case generated.OCR2ChainTypeSolana: + var accounts []string + for _, key := range keys.SolanaKeys.GetResults() { + accounts = append(accounts, key.Id) + } + return accounts, nil + default: + return nil, fmt.Errorf("unsupported chainType %v", chainType) + } +} + func (c *client) GetJob(ctx context.Context, id string) (*generated.GetJobResponse, error) { return generated.GetJob(ctx, c.gqlClient, id) } diff --git a/deployment/environment/web/sdk/internal/generated/generated.go b/deployment/environment/web/sdk/internal/generated/generated.go index 68ab3e48e4f..7b16e4a1e3f 100644 --- a/deployment/environment/web/sdk/internal/generated/generated.go +++ b/deployment/environment/web/sdk/internal/generated/generated.go @@ -1887,6 +1887,58 @@ type FetchCSAKeysResponse struct { // GetCsaKeys returns FetchCSAKeysResponse.CsaKeys, and is useful for accessing the field via an interface. func (v *FetchCSAKeysResponse) GetCsaKeys() FetchCSAKeysCsaKeysCSAKeysPayload { return v.CsaKeys } +// FetchKeysAptosKeysAptosKeysPayload includes the requested fields of the GraphQL type AptosKeysPayload. +type FetchKeysAptosKeysAptosKeysPayload struct { + Results []FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey `json:"results"` +} + +// GetResults returns FetchKeysAptosKeysAptosKeysPayload.Results, and is useful for accessing the field via an interface. +func (v *FetchKeysAptosKeysAptosKeysPayload) GetResults() []FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey { + return v.Results +} + +// FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey includes the requested fields of the GraphQL type AptosKey. +type FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey struct { + Id string `json:"id"` + Account string `json:"account"` +} + +// GetId returns FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey.Id, and is useful for accessing the field via an interface. +func (v *FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey) GetId() string { return v.Id } + +// GetAccount returns FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey.Account, and is useful for accessing the field via an interface. +func (v *FetchKeysAptosKeysAptosKeysPayloadResultsAptosKey) GetAccount() string { return v.Account } + +// FetchKeysResponse is returned by FetchKeys on success. +type FetchKeysResponse struct { + SolanaKeys FetchKeysSolanaKeysSolanaKeysPayload `json:"solanaKeys"` + AptosKeys FetchKeysAptosKeysAptosKeysPayload `json:"aptosKeys"` +} + +// GetSolanaKeys returns FetchKeysResponse.SolanaKeys, and is useful for accessing the field via an interface. +func (v *FetchKeysResponse) GetSolanaKeys() FetchKeysSolanaKeysSolanaKeysPayload { return v.SolanaKeys } + +// GetAptosKeys returns FetchKeysResponse.AptosKeys, and is useful for accessing the field via an interface. +func (v *FetchKeysResponse) GetAptosKeys() FetchKeysAptosKeysAptosKeysPayload { return v.AptosKeys } + +// FetchKeysSolanaKeysSolanaKeysPayload includes the requested fields of the GraphQL type SolanaKeysPayload. +type FetchKeysSolanaKeysSolanaKeysPayload struct { + Results []FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey `json:"results"` +} + +// GetResults returns FetchKeysSolanaKeysSolanaKeysPayload.Results, and is useful for accessing the field via an interface. +func (v *FetchKeysSolanaKeysSolanaKeysPayload) GetResults() []FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey { + return v.Results +} + +// FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey includes the requested fields of the GraphQL type SolanaKey. +type FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey struct { + Id string `json:"id"` +} + +// GetId returns FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey.Id, and is useful for accessing the field via an interface. +func (v *FetchKeysSolanaKeysSolanaKeysPayloadResultsSolanaKey) GetId() string { return v.Id } + // FetchOCR2KeyBundlesOcr2KeyBundlesOCR2KeyBundlesPayload includes the requested fields of the GraphQL type OCR2KeyBundlesPayload. type FetchOCR2KeyBundlesOcr2KeyBundlesOCR2KeyBundlesPayload struct { Results []FetchOCR2KeyBundlesOcr2KeyBundlesOCR2KeyBundlesPayloadResultsOCR2KeyBundle `json:"results"` @@ -5660,6 +5712,45 @@ func FetchCSAKeys( return &data_, err_ } +// The query or mutation executed by FetchKeys. +const FetchKeys_Operation = ` +query FetchKeys { + solanaKeys { + results { + id + } + } + aptosKeys { + results { + id + account + } + } +} +` + +func FetchKeys( + ctx_ context.Context, + client_ graphql.Client, +) (*FetchKeysResponse, error) { + req_ := &graphql.Request{ + OpName: "FetchKeys", + Query: FetchKeys_Operation, + } + var err_ error + + var data_ FetchKeysResponse + resp_ := &graphql.Response{Data: &data_} + + err_ = client_.MakeRequest( + ctx_, + req_, + resp_, + ) + + return &data_, err_ +} + // The query or mutation executed by FetchOCR2KeyBundles. const FetchOCR2KeyBundles_Operation = ` query FetchOCR2KeyBundles { diff --git a/deployment/environment/web/sdk/internal/genqlient.graphql b/deployment/environment/web/sdk/internal/genqlient.graphql index 06baf4f7913..4c998a4f6a6 100644 --- a/deployment/environment/web/sdk/internal/genqlient.graphql +++ b/deployment/environment/web/sdk/internal/genqlient.graphql @@ -45,6 +45,20 @@ query FetchAccounts { } } +query FetchKeys { + solanaKeys { + results { + id + } + } + aptosKeys { + results { + id + account + } + } +} + ##################### # ocr2KeyBundles ##################### @@ -456,4 +470,4 @@ mutation UpdateJobProposalSpecDefinition( code } } -} \ No newline at end of file +} diff --git a/deployment/go.mod b/deployment/go.mod index 26342d19ca2..594c0043006 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -31,6 +31,7 @@ require ( github.com/testcontainers/testcontainers-go v0.34.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/sync v0.8.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 @@ -475,7 +476,6 @@ require ( go4.org/netipx v0.0.0-20230125063823-8449b0a6169f // indirect golang.org/x/arch v0.11.0 // indirect golang.org/x/crypto v0.28.0 // indirect - golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect diff --git a/deployment/keystone/changeset/deploy_ocr3.go b/deployment/keystone/changeset/deploy_ocr3.go index 016eaa97d1f..e0edf4a4440 100644 --- a/deployment/keystone/changeset/deploy_ocr3.go +++ b/deployment/keystone/changeset/deploy_ocr3.go @@ -5,7 +5,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) @@ -27,9 +26,8 @@ func DeployOCR3(env deployment.Environment, config interface{}) (deployment.Chan return deployment.ChangesetOutput{AddressBook: ab}, nil } -func ConfigureOCR3Contract(lggr logger.Logger, env deployment.Environment, ab deployment.AddressBook, registryChainSel uint64, nodes []*models.Node, cfg kslib.OracleConfigWithSecrets) (deployment.ChangesetOutput, error) { - - err := kslib.ConfigureOCR3ContractFromCLO(&env, registryChainSel, nodes, ab, &cfg) +func ConfigureOCR3Contract(lggr logger.Logger, env deployment.Environment, ab deployment.AddressBook, registryChainSel uint64, nodes []string, cfg kslib.OracleConfigWithSecrets) (deployment.ChangesetOutput, error) { + err := kslib.ConfigureOCR3ContractFromJD(&env, registryChainSel, nodes, ab, &cfg) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to configure OCR3Capability: %w", err) } diff --git a/deployment/keystone/changeset/internal/update_don_test.go b/deployment/keystone/changeset/internal/update_don_test.go index baedda5e93d..12ccfe290b1 100644 --- a/deployment/keystone/changeset/internal/update_don_test.go +++ b/deployment/keystone/changeset/internal/update_don_test.go @@ -4,14 +4,14 @@ import ( "bytes" "math/big" "sort" - "strconv" "testing" "github.com/ethereum/go-ethereum/common" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-common/pkg/logger" + nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" + "github.com/smartcontractkit/chainlink/deployment/keystone" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" kscs "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" @@ -95,18 +95,19 @@ func TestUpdateDon(t *testing.T) { t.Run("empty", func(t *testing.T) { cfg := setupUpdateDonTestConfig{ - dons: []kslib.DonCapabilities{ + dons: []kslib.DonInfo{ { - Name: "don 1", - Nops: []*models.NodeOperator{ - { - Name: "nop 1", - Nodes: []*models.Node{node_1, node_2, node_3, node_4}, - }, - }, + Name: "don 1", + Nodes: []keystone.Node{node_1, node_2, node_3, node_4}, Capabilities: []kcr.CapabilitiesRegistryCapability{cap_A}, }, }, + nops: []keystone.NOP{ + { + Name: "nop 1", + Nodes: []string{node_1.ID, node_2.ID, node_3.ID, node_4.ID}, + }, + }, } testCfg := setupUpdateDonTest(t, lggr, cfg) @@ -169,26 +170,24 @@ type minimalNodeCfg struct { admin common.Address } -func newNode(t *testing.T, cfg minimalNodeCfg) *models.Node { +func newNode(t *testing.T, cfg minimalNodeCfg) keystone.Node { t.Helper() - return &models.Node{ + return keystone.Node{ ID: cfg.id, PublicKey: &cfg.pubKey, - ChainConfigs: []*models.NodeChainConfig{ + ChainConfigs: []*nodev1.ChainConfig{ { - ID: "test chain", - Network: &models.Network{ - ID: "test network 1", - ChainID: strconv.FormatUint(cfg.registryChain.EvmChainID, 10), - ChainType: models.ChainTypeEvm, + Chain: &nodev1.Chain{ + Id: "test chain", + Type: nodev1.ChainType_CHAIN_TYPE_EVM, }, AdminAddress: cfg.admin.String(), - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: cfg.p2p.PeerID().String(), + Ocr2Config: &nodev1.OCR2Config{ + P2PKeyBundle: &nodev1.OCR2Config_P2PKeyBundle{ + PeerId: cfg.p2p.PeerID().String(), }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ + OcrKeyBundle: &nodev1.OCR2Config_OCRKeyBundle{ OnchainSigningAddress: cfg.signingAddr, }, }, @@ -198,7 +197,8 @@ func newNode(t *testing.T, cfg minimalNodeCfg) *models.Node { } type setupUpdateDonTestConfig struct { - dons []kslib.DonCapabilities + dons []kslib.DonInfo + nops []keystone.NOP } type setupUpdateDonTestResult struct { @@ -208,28 +208,19 @@ type setupUpdateDonTestResult struct { func setupUpdateDonTest(t *testing.T, lggr logger.Logger, cfg setupUpdateDonTestConfig) *kstest.SetupTestRegistryResponse { t.Helper() - req := newSetupTestRegistryRequest(t, cfg.dons) + req := newSetupTestRegistryRequest(t, cfg.dons, cfg.nops) return kstest.SetupTestRegistry(t, lggr, req) } -func newSetupTestRegistryRequest(t *testing.T, dons []kslib.DonCapabilities) *kstest.SetupTestRegistryRequest { +func newSetupTestRegistryRequest(t *testing.T, dons []kslib.DonInfo, nops []keystone.NOP) *kstest.SetupTestRegistryRequest { t.Helper() - allNops := make(map[string]*models.NodeOperator) + nodes := make(map[string]keystone.Node) for _, don := range dons { - for _, nop := range don.Nops { - nop := nop - n, exists := allNops[nop.ID] - if exists { - nop.Nodes = append(n.Nodes, nop.Nodes...) - } - allNops[nop.ID] = nop + for _, node := range don.Nodes { + nodes[node.ID] = node } } - var nops []*models.NodeOperator - for _, nop := range allNops { - nops = append(nops, nop) - } - nopsToNodes := makeNopToNodes(t, nops) + nopsToNodes := makeNopToNodes(t, nops, nodes) testDons := makeTestDon(t, dons) p2pToCapabilities := makeP2PToCapabilities(t, dons) req := &kstest.SetupTestRegistryRequest{ @@ -240,46 +231,45 @@ func newSetupTestRegistryRequest(t *testing.T, dons []kslib.DonCapabilities) *ks return req } -func makeNopToNodes(t *testing.T, cloNops []*models.NodeOperator) map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc { +func makeNopToNodes(t *testing.T, nops []keystone.NOP, nodes map[string]keystone.Node) map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc { nopToNodes := make(map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc) - for _, nop := range cloNops { + for _, nop := range nops { // all chain configs are the same wrt admin address & node keys // so we can just use the first one crnop := kcr.CapabilitiesRegistryNodeOperator{ Name: nop.Name, - Admin: common.HexToAddress(nop.Nodes[0].ChainConfigs[0].AdminAddress), + Admin: common.HexToAddress(nodes[nop.Nodes[0]].ChainConfigs[0].AdminAddress), } - var nodes []*internal.P2PSignerEnc - for _, node := range nop.Nodes { + var signers []*internal.P2PSignerEnc + for _, nodeID := range nop.Nodes { + node := nodes[nodeID] require.NotNil(t, node.PublicKey, "public key is nil %s", node.ID) // all chain configs are the same wrt admin address & node keys - p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) + p, err := kscs.NewP2PSignerEncFromJD(node.ChainConfigs[0], *node.PublicKey) require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) - nodes = append(nodes, p) + signers = append(signers, p) } - nopToNodes[crnop] = nodes + nopToNodes[crnop] = signers } return nopToNodes } -func makeP2PToCapabilities(t *testing.T, dons []kslib.DonCapabilities) map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability { +func makeP2PToCapabilities(t *testing.T, dons []kslib.DonInfo) map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability { p2pToCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) for _, don := range dons { - for _, nop := range don.Nops { - for _, node := range nop.Nodes { - for _, cap := range don.Capabilities { - p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) - require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) - p2pToCapabilities[p.P2PKey] = append(p2pToCapabilities[p.P2PKey], cap) - } + for _, node := range don.Nodes { + for _, cap := range don.Capabilities { + p, err := kscs.NewP2PSignerEncFromJD(node.ChainConfigs[0], *node.PublicKey) + require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) + p2pToCapabilities[p.P2PKey] = append(p2pToCapabilities[p.P2PKey], cap) } } } return p2pToCapabilities } -func makeTestDon(t *testing.T, dons []kslib.DonCapabilities) []kstest.Don { +func makeTestDon(t *testing.T, dons []kslib.DonInfo) []kstest.Don { out := make([]kstest.Don, len(dons)) for i, don := range dons { out[i] = testDon(t, don) @@ -287,16 +277,14 @@ func makeTestDon(t *testing.T, dons []kslib.DonCapabilities) []kstest.Don { return out } -func testDon(t *testing.T, don kslib.DonCapabilities) kstest.Don { +func testDon(t *testing.T, don kslib.DonInfo) kstest.Don { var p2pids []p2pkey.PeerID - for _, nop := range don.Nops { - for _, node := range nop.Nodes { - // all chain configs are the same wrt admin address & node keys - // so we can just use the first one - p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) - require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) - p2pids = append(p2pids, p.P2PKey) - } + for _, node := range don.Nodes { + // all chain configs are the same wrt admin address & node keys + // so we can just use the first one + p, err := kscs.NewP2PSignerEncFromJD(node.ChainConfigs[0], *node.PublicKey) + require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) + p2pids = append(p2pids, p.P2PKey) } var capabilityConfigs []internal.CapabilityConfig diff --git a/deployment/keystone/changeset/types.go b/deployment/keystone/changeset/types.go index e8a86fa4272..fb609041792 100644 --- a/deployment/keystone/changeset/types.go +++ b/deployment/keystone/changeset/types.go @@ -6,24 +6,17 @@ import ( "fmt" v1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" - "github.com/smartcontractkit/chainlink/deployment/environment/clo" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" ) -func NewP2PSignerEncFromCLO(cc *models.NodeChainConfig, pubkey string) (*P2PSignerEnc, error) { - ccfg := clo.NewChainConfig(cc) - var pubkeyB [32]byte - if _, err := hex.Decode(pubkeyB[:], []byte(pubkey)); err != nil { - return nil, fmt.Errorf("failed to decode pubkey %s: %w", pubkey, err) - } - return newP2PSignerEncFromJD(ccfg, pubkeyB) -} - -func newP2PSignerEncFromJD(ccfg *v1.ChainConfig, pubkey [32]byte) (*P2PSignerEnc, error) { +func NewP2PSignerEncFromJD(ccfg *v1.ChainConfig, pubkeyStr string) (*P2PSignerEnc, error) { if ccfg == nil { return nil, errors.New("nil ocr2config") } + var pubkey [32]byte + if _, err := hex.Decode(pubkey[:], []byte(pubkeyStr)); err != nil { + return nil, fmt.Errorf("failed to decode pubkey %s: %w", pubkey, err) + } ocfg := ccfg.Ocr2Config p2p := p2pkey.PeerID{} if err := p2p.UnmarshalString(ocfg.P2PKeyBundle.PeerId); err != nil { diff --git a/deployment/keystone/changeset/update_node_capabilities.go b/deployment/keystone/changeset/update_node_capabilities.go index 09cf351cc85..a2f52be989c 100644 --- a/deployment/keystone/changeset/update_node_capabilities.go +++ b/deployment/keystone/changeset/update_node_capabilities.go @@ -6,7 +6,7 @@ import ( chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" + "github.com/smartcontractkit/chainlink/deployment/keystone" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" @@ -18,7 +18,7 @@ var _ deployment.ChangeSet = UpdateNodeCapabilities type P2PSignerEnc = internal.P2PSignerEnc -func NewP2PSignerEnc(n *models.Node, registryChainSel uint64) (*P2PSignerEnc, error) { +func NewP2PSignerEnc(n *keystone.Node, registryChainSel uint64) (*P2PSignerEnc, error) { p2p, signer, enc, err := kslib.ExtractKeys(n, registryChainSel) if err != nil { return nil, fmt.Errorf("failed to extract keys: %w", err) diff --git a/deployment/keystone/deploy.go b/deployment/keystone/deploy.go index 8838312121a..737850cc0fa 100644 --- a/deployment/keystone/deploy.go +++ b/deployment/keystone/deploy.go @@ -7,15 +7,18 @@ import ( "encoding/hex" "errors" "fmt" + "slices" "sort" "strings" "time" + "github.com/AlekSi/pointer" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/rpc" + nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/shared/ptypes" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" @@ -90,8 +93,13 @@ func ConfigureContracts(ctx context.Context, lggr logger.Logger, req ConfigureCo return nil, fmt.Errorf("failed to configure registry: %w", err) } + donInfos, err := DonInfos(req.Dons, req.Env.Offchain) + if err != nil { + return nil, fmt.Errorf("failed to get don infos: %w", err) + } + // now we have the capability registry set up we need to configure the forwarder contracts and the OCR3 contract - dons, err := joinInfoAndNodes(cfgRegistryResp.DonInfos, req.Dons, req.RegistryChainSel) + dons, err := joinInfoAndNodes(cfgRegistryResp.DonInfos, donInfos, req.RegistryChainSel) if err != nil { return nil, fmt.Errorf("failed to assimilate registry to Dons: %w", err) } @@ -137,6 +145,91 @@ func DeployContracts(lggr logger.Logger, e *deployment.Environment, chainSel uin }, nil } +// DonInfo is DonCapabilities, but expanded to contain node information +type DonInfo struct { + Name string + Nodes []Node + Capabilities []kcr.CapabilitiesRegistryCapability // every capability is hosted on each node +} + +// TODO: merge with deployment/environment.go Node +type Node struct { + ID string + P2PID string + Name string + PublicKey *string + ChainConfigs []*nodev1.ChainConfig +} + +// TODO: merge with deployment/environment.go NodeInfo, we currently lookup based on p2p_id, and chain-selectors needs non-EVM support +func NodesFromJD(name string, nodeIDs []string, jd deployment.OffchainClient) ([]Node, error) { + // lookup nodes based on p2p_ids + var nodes []Node + selector := strings.Join(nodeIDs, ",") + nodesFromJD, err := jd.ListNodes(context.Background(), &nodev1.ListNodesRequest{ + Filter: &nodev1.ListNodesRequest_Filter{ + Enabled: 1, + Selectors: []*ptypes.Selector{ + { + Key: "p2p_id", + Op: ptypes.SelectorOp_IN, + Value: pointer.ToString(selector), + }, + }, + }, + }) + if err != nil { + return nil, err + } + for _, nodeP2PID := range nodeIDs { + // TODO: Filter should accept multiple nodes + nodeChainConfigs, err := jd.ListNodeChainConfigs(context.Background(), &nodev1.ListNodeChainConfigsRequest{Filter: &nodev1.ListNodeChainConfigsRequest_Filter{ + NodeIds: []string{nodeP2PID}, + }}) + if err != nil { + return nil, err + } + idx := slices.IndexFunc(nodesFromJD.GetNodes(), func(node *nodev1.Node) bool { + return slices.ContainsFunc(node.Labels, func(label *ptypes.Label) bool { + return label.Key == "p2p_id" && *label.Value == nodeP2PID + }) + }) + if idx < 0 { + return nil, fmt.Errorf("node %v not found", nodeP2PID) + } + jdNode := nodesFromJD.Nodes[idx] + + nodes = append(nodes, Node{ + ID: jdNode.Id, + P2PID: nodeP2PID, + Name: name, + PublicKey: &jdNode.PublicKey, + ChainConfigs: nodeChainConfigs.GetChainConfigs(), + }) + } + return nodes, nil +} + +func DonInfos(dons []DonCapabilities, jd deployment.OffchainClient) ([]DonInfo, error) { + var donInfos []DonInfo + for _, don := range dons { + var nodeIDs []string + for _, nop := range don.Nops { + nodeIDs = append(nodeIDs, nop.Nodes...) + } + nodes, err := NodesFromJD(don.Name, nodeIDs, jd) + if err != nil { + return nil, err + } + donInfos = append(donInfos, DonInfo{ + Name: don.Name, + Nodes: nodes, + Capabilities: don.Capabilities, + }) + } + return donInfos, nil +} + // ConfigureRegistry configures the registry contract with the given DONS and their capabilities // the address book is required to contain the addresses of the deployed registry contract func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureContractsRequest, addrBook deployment.AddressBook) (*ConfigureContractsResponse, error) { @@ -153,6 +246,11 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon return nil, fmt.Errorf("failed to get contract sets: %w", err) } + donInfos, err := DonInfos(req.Dons, req.Env.Offchain) + if err != nil { + return nil, fmt.Errorf("failed to get don infos: %w", err) + } + // ensure registry is deployed and get the registry contract and chain var registry *kcr.CapabilitiesRegistry registryChainContracts, ok := contractSetsResp.ContractSets[req.RegistryChainSel] @@ -167,15 +265,15 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon // all the subsequent calls to the registry are in terms of nodes // compute the mapping of dons to their nodes for reuse in various registry calls - donToOcr2Nodes, err := mapDonsToNodes(req.Dons, true, req.RegistryChainSel) + donToOcr2Nodes, err := mapDonsToNodes(donInfos, true, req.RegistryChainSel) if err != nil { return nil, fmt.Errorf("failed to map dons to nodes: %w", err) } // TODO: we can remove this abstractions and refactor the functions that accept them to accept []DonCapabilities // they are unnecessary indirection - donToCapabilities := mapDonsToCaps(req.Dons) - nodeIdToNop, err := nodesToNops(req.Dons, req.RegistryChainSel) + donToCapabilities := mapDonsToCaps(donInfos) + nodeIdToNop, err := nodesToNops(donInfos, req.RegistryChainSel) if err != nil { return nil, fmt.Errorf("failed to map nodes to nops: %w", err) } @@ -220,6 +318,8 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon } lggr.Infow("registered nodes", "nodes", nodesResp.nodeIDToParams) + // TODO: annotate nodes with node_operator_id in JD? + // register DONS donsResp, err := registerDons(lggr, registerDonsRequest{ registry: registry, @@ -314,7 +414,7 @@ func ConfigureOCR3Contract(env *deployment.Environment, chainSel uint64, dons [] return nil } -func ConfigureOCR3ContractFromCLO(env *deployment.Environment, chainSel uint64, nodes []*models.Node, addrBook deployment.AddressBook, cfg *OracleConfigWithSecrets) error { +func ConfigureOCR3ContractFromJD(env *deployment.Environment, chainSel uint64, nodeIDs []string, addrBook deployment.AddressBook, cfg *OracleConfigWithSecrets) error { registryChain, ok := env.Chains[chainSel] if !ok { return fmt.Errorf("chain %d not found in environment", chainSel) @@ -334,9 +434,13 @@ func ConfigureOCR3ContractFromCLO(env *deployment.Environment, chainSel uint64, if contract == nil { return fmt.Errorf("no ocr3 contract found for chain %d", chainSel) } + nodes, err := NodesFromJD("nodes", nodeIDs, env.Offchain) + if err != nil { + return err + } var ocr2nodes []*ocr2Node for _, node := range nodes { - n, err := newOcr2NodeFromClo(node, chainSel) + n, err := newOcr2NodeFromClo(&node, chainSel) if err != nil { return fmt.Errorf("failed to create ocr2 node from clo node: %w", err) } diff --git a/deployment/keystone/deploy_test.go b/deployment/keystone/deploy_test.go index 211e273c38e..e362270c110 100644 --- a/deployment/keystone/deploy_test.go +++ b/deployment/keystone/deploy_test.go @@ -4,11 +4,14 @@ import ( "encoding/json" "fmt" "os" + "strconv" "testing" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/assert" "github.com/test-go/testify/require" + "go.uber.org/zap/zapcore" + "golang.org/x/exp/maps" chainsel "github.com/smartcontractkit/chain-selectors" @@ -17,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/environment/clo" "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" + "github.com/smartcontractkit/chainlink/deployment/environment/memory" "github.com/smartcontractkit/chainlink/deployment/keystone" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/logger" @@ -25,6 +29,201 @@ import ( func TestDeploy(t *testing.T) { lggr := logger.TestLogger(t) + // sepolia; all nodes are on the this chain + sepoliaChainId := uint64(11155111) + sepoliaArbitrumChainId := uint64(421614) + + sepoliaChainSel, err := chainsel.SelectorFromChainId(sepoliaChainId) + require.NoError(t, err) + // sepoliaArbitrumChainSel, err := chainsel.SelectorFromChainId(sepoliaArbitrumChainId) + // require.NoError(t, err) + // aptosChainSel := uint64(999) // TODO: + + crConfig := deployment.CapabilityRegistryConfig{ + EVMChainID: sepoliaChainId, + Contract: [20]byte{}, + } + + evmChains := memory.NewMemoryChainsWithChainIDs(t, []uint64{sepoliaChainId, sepoliaArbitrumChainId}) + // aptosChain := memory.NewMemoryChain(t, aptosChainSel) + + wfChains := map[uint64]deployment.Chain{} + wfChains[sepoliaChainSel] = evmChains[sepoliaChainSel] + // wfChains[aptosChainSel] = aptosChain + wfNodes := memory.NewNodes(t, zapcore.InfoLevel, wfChains, 4, 0, crConfig) + require.Len(t, wfNodes, 4) + + cwNodes := memory.NewNodes(t, zapcore.InfoLevel, evmChains, 4, 0, crConfig) + + assetChains := map[uint64]deployment.Chain{} + assetChains[sepoliaChainSel] = evmChains[sepoliaChainSel] + assetNodes := memory.NewNodes(t, zapcore.InfoLevel, assetChains, 4, 0, crConfig) + require.Len(t, assetNodes, 4) + + // TODO: partition nodes into multiple nops + + wfDon := keystone.DonCapabilities{ + Name: keystone.WFDonName, + Nops: []keystone.NOP{ + { + Name: "nop 1", + Nodes: maps.Keys(wfNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.OCR3Cap}, + } + cwDon := keystone.DonCapabilities{ + Name: keystone.TargetDonName, + Nops: []keystone.NOP{ + { + Name: "nop 2", + Nodes: maps.Keys(cwNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.WriteChainCap}, + } + assetDon := keystone.DonCapabilities{ + Name: keystone.StreamDonName, + Nops: []keystone.NOP{ + { + Name: "nop 3", + Nodes: maps.Keys(assetNodes), + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.StreamTriggerCap}, + } + + allChains := make(map[uint64]deployment.Chain) + maps.Copy(allChains, evmChains) + // allChains[aptosChainSel] = aptosChain + + allNodes := make(map[string]memory.Node) + maps.Copy(allNodes, wfNodes) + maps.Copy(allNodes, cwNodes) + maps.Copy(allNodes, assetNodes) + env := memory.NewMemoryEnvironmentFromChainsNodes(t, lggr, allChains, allNodes) + + var ocr3Config = keystone.OracleConfigWithSecrets{ + OracleConfig: keystone.OracleConfig{ + MaxFaultyOracles: len(wfNodes) / 3, + }, + OCRSecrets: deployment.XXXGenerateTestOCRSecrets(), + } + + ctx := tests.Context(t) + // explicitly deploy the contracts + cs, err := keystone.DeployContracts(lggr, &env, sepoliaChainSel) + require.NoError(t, err) + env.ExistingAddresses = cs.AddressBook + deployReq := keystone.ConfigureContractsRequest{ + RegistryChainSel: sepoliaChainSel, + Env: &env, + OCR3Config: &ocr3Config, + Dons: []keystone.DonCapabilities{wfDon, cwDon, assetDon}, + DoContractDeploy: false, + } + deployResp, err := keystone.ConfigureContracts(ctx, lggr, deployReq) + require.NoError(t, err) + ad := deployResp.Changeset.AddressBook + addrs, err := ad.Addresses() + require.NoError(t, err) + lggr.Infow("Deployed Keystone contracts", "address book", addrs) + + // all contracts on home chain + homeChainAddrs, err := ad.AddressesForChain(sepoliaChainSel) + require.NoError(t, err) + require.Len(t, homeChainAddrs, 3) + // only forwarder on non-home chain + for sel := range env.Chains { + chainAddrs, err := ad.AddressesForChain(sel) + require.NoError(t, err) + if sel != sepoliaChainSel { + require.Len(t, chainAddrs, 1) + } else { + require.Len(t, chainAddrs, 3) + } + containsForwarder := false + for _, tv := range chainAddrs { + if tv.Type == keystone.KeystoneForwarder { + containsForwarder = true + break + } + } + require.True(t, containsForwarder, "no forwarder found in %v on chain %d for target don", chainAddrs, sel) + } + req := &keystone.GetContractSetsRequest{ + Chains: env.Chains, + AddressBook: ad, + } + + contractSetsResp, err := keystone.GetContractSets(lggr, req) + require.NoError(t, err) + require.Len(t, contractSetsResp.ContractSets, len(env.Chains)) + // check the registry + regChainContracts, ok := contractSetsResp.ContractSets[sepoliaChainSel] + require.True(t, ok) + gotRegistry := regChainContracts.CapabilitiesRegistry + require.NotNil(t, gotRegistry) + // contract reads + gotDons, err := gotRegistry.GetDONs(&bind.CallOpts{}) + if err != nil { + err = keystone.DecodeErr(kcr.CapabilitiesRegistryABI, err) + require.Fail(t, fmt.Sprintf("failed to get Dons from registry at %s: %s", gotRegistry.Address().String(), err)) + } + require.NoError(t, err) + assert.Len(t, gotDons, len(deployReq.Dons)) + + for n, info := range deployResp.DonInfos { + found := false + for _, gdon := range gotDons { + if gdon.Id == info.Id { + found = true + assert.EqualValues(t, info, gdon) + break + } + } + require.True(t, found, "don %s not found in registry", n) + } + // check the forwarder + for _, cs := range contractSetsResp.ContractSets { + forwarder := cs.Forwarder + require.NotNil(t, forwarder) + // any read to ensure that the contract is deployed correctly + _, err := forwarder.Owner(&bind.CallOpts{}) + require.NoError(t, err) + // TODO expand this test; there is no get method on the forwarder so unclear how to test it + } + // check the ocr3 contract + for chainSel, cs := range contractSetsResp.ContractSets { + if chainSel != sepoliaChainSel { + require.Nil(t, cs.OCR3) + continue + } + require.NotNil(t, cs.OCR3) + // any read to ensure that the contract is deployed correctly + _, err := cs.OCR3.LatestConfigDetails(&bind.CallOpts{}) + require.NoError(t, err) + } +} + +// TODO: Deprecated, remove everything below that leverages CLO + +func nodeOperatorsToIDs(nops []*models.NodeOperator) (nodeIDs []keystone.NOP) { + for _, nop := range nops { + nodeOperator := keystone.NOP{ + Name: nop.Name, + } + for _, node := range nop.Nodes { + nodeOperator.Nodes = append(nodeOperator.Nodes, node.ID) + } + nodeIDs = append(nodeIDs, nodeOperator) + } + return nodeIDs +} + +func TestDeployCLO(t *testing.T) { + lggr := logger.TestLogger(t) + wfNops := loadTestNops(t, "testdata/workflow_nodes.json") cwNops := loadTestNops(t, "testdata/chain_writer_nodes.json") assetNops := loadTestNops(t, "testdata/asset_nodes.json") @@ -35,23 +234,65 @@ func TestDeploy(t *testing.T) { require.Len(t, assetNops, 16) requireChains(t, assetNops, []models.ChainType{models.ChainTypeEvm}) + wfNodes := nodeOperatorsToIDs(wfNops) + cwNodes := nodeOperatorsToIDs(cwNops) + assetNodes := nodeOperatorsToIDs(assetNops) + wfDon := keystone.DonCapabilities{ Name: keystone.WFDonName, - Nops: wfNops, + Nops: wfNodes, Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.OCR3Cap}, } cwDon := keystone.DonCapabilities{ Name: keystone.TargetDonName, - Nops: cwNops, + Nops: cwNodes, Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.WriteChainCap}, } assetDon := keystone.DonCapabilities{ Name: keystone.StreamDonName, - Nops: assetNops, + Nops: assetNodes, Capabilities: []kcr.CapabilitiesRegistryCapability{keystone.StreamTriggerCap}, } - env := makeMultiDonTestEnv(t, lggr, []keystone.DonCapabilities{wfDon, cwDon, assetDon}) + var allNops []*models.NodeOperator + allNops = append(allNops, wfNops...) + allNops = append(allNops, cwNops...) + allNops = append(allNops, assetNops...) + + chains := make(map[uint64]struct{}) + for _, nop := range allNops { + for _, node := range nop.Nodes { + for _, chain := range node.ChainConfigs { + // chain selector lib doesn't support chain id 2 and we don't use it in tests + // because it's not an evm chain + if chain.Network.ChainID == "2" { // aptos chain + continue + } + id, err := strconv.ParseUint(chain.Network.ChainID, 10, 64) + require.NoError(t, err, "failed to parse chain id to uint64") + chains[id] = struct{}{} + } + } + } + var chainIDs []uint64 + for c := range chains { + chainIDs = append(chainIDs, c) + } + allChains := memory.NewMemoryChainsWithChainIDs(t, chainIDs) + + env := &deployment.Environment{ + Name: "CLO", + ExistingAddresses: deployment.NewMemoryAddressBook(), + Offchain: clo.NewJobClient(lggr, allNops), + Chains: allChains, + Logger: lggr, + } + // assume that all the nodes in the provided input nops are part of the don + for _, nop := range allNops { + for _, node := range nop.Nodes { + env.NodeIDs = append(env.NodeIDs, node.ID) + } + } // sepolia; all nodes are on the this chain registryChainSel, err := chainsel.SelectorFromChainId(11155111) @@ -178,25 +419,6 @@ func requireChains(t *testing.T, donNops []*models.NodeOperator, cs []models.Cha } } -func makeMultiDonTestEnv(t *testing.T, lggr logger.Logger, dons []keystone.DonCapabilities) *deployment.Environment { - var donToEnv = make(map[string]*deployment.Environment) - // chain selector lib doesn't support chain id 2 and we don't use it in tests - // because it's not an evm chain - ignoreAptos := func(c *models.NodeChainConfig) bool { - return c.Network.ChainID == "2" // aptos chain - } - for _, don := range dons { - env := clo.NewDonEnvWithMemoryChains(t, clo.DonEnvConfig{ - DonName: don.Name, - Nops: don.Nops, - Logger: lggr, - }, ignoreAptos) - donToEnv[don.Name] = env - } - menv := clo.NewTestEnv(t, lggr, donToEnv) - return menv.Flatten("testing-env") -} - func loadTestNops(t *testing.T, pth string) []*models.NodeOperator { f, err := os.ReadFile(pth) require.NoError(t, err) diff --git a/deployment/keystone/types.go b/deployment/keystone/types.go index 18967ccf445..dd93c1889ec 100644 --- a/deployment/keystone/types.go +++ b/deployment/keystone/types.go @@ -13,7 +13,6 @@ import ( chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink/deployment" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" v1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" @@ -100,8 +99,7 @@ func (o *ocr2Node) toNodeKeys() NodeKeys { AptosOnchainPublicKey: aptosOnchainPublicKey, } } - -func newOcr2NodeFromClo(n *models.Node, registryChainSel uint64) (*ocr2Node, error) { +func newOcr2NodeFromClo(n *Node, registryChainSel uint64) (*ocr2Node, error) { if n.PublicKey == nil { return nil, errors.New("no public key") } @@ -110,21 +108,21 @@ func newOcr2NodeFromClo(n *models.Node, registryChainSel uint64) (*ocr2Node, err return nil, errors.New("no chain configs") } // all nodes should have an evm chain config, specifically the registry chain - evmCC, err := registryChainConfig(n.ChainConfigs, chaintype.EVM, registryChainSel) + evmCC, err := registryChainConfig(n.ChainConfigs, v1.ChainType_CHAIN_TYPE_EVM, registryChainSel) if err != nil { return nil, fmt.Errorf("failed to get registry chain config for sel %d: %w", registryChainSel, err) } cfgs := map[chaintype.ChainType]*v1.ChainConfig{ chaintype.EVM: evmCC, } - aptosCC, exists := firstChainConfigByType(n.ChainConfigs, chaintype.Aptos) + aptosCC, exists := firstChainConfigByType(n.ChainConfigs, v1.ChainType_CHAIN_TYPE_APTOS) if exists { cfgs[chaintype.Aptos] = aptosCC } - return newOcr2Node(n.ID, cfgs, *n.PublicKey) + return newOcr2Node(n.P2PID, cfgs, *n.PublicKey) } -func ExtractKeys(n *models.Node, registerChainSel uint64) (p2p p2pkey.PeerID, signer [32]byte, encPubKey [32]byte, err error) { +func ExtractKeys(n *Node, registerChainSel uint64) (p2p p2pkey.PeerID, signer [32]byte, encPubKey [32]byte, err error) { orc2n, err := newOcr2NodeFromClo(n, registerChainSel) if err != nil { return p2p, signer, encPubKey, fmt.Errorf("failed to create ocr2 node for node %s: %w", n.ID, err) @@ -201,26 +199,30 @@ func makeNodeKeysSlice(nodes []*ocr2Node) []NodeKeys { return out } +type NOP struct { + Name string + Nodes []string // peerID +} + // DonCapabilities is a set of capabilities hosted by a set of node operators // in is in a convenient form to handle the CLO representation of the nop data type DonCapabilities struct { Name string - Nops []*models.NodeOperator // each nop is a node operator and may have multiple nodes + Nops []NOP Capabilities []kcr.CapabilitiesRegistryCapability // every capability is hosted on each nop } // map the node id to the NOP -func (dc DonCapabilities) nodeIdToNop(cs uint64) (map[string]capabilities_registry.CapabilitiesRegistryNodeOperator, error) { +func (dc DonInfo) nodeIdToNop(cs uint64) (map[string]capabilities_registry.CapabilitiesRegistryNodeOperator, error) { out := make(map[string]capabilities_registry.CapabilitiesRegistryNodeOperator) - for _, nop := range dc.Nops { - for _, node := range nop.Nodes { - a, err := AdminAddress(node, cs) - if err != nil { - return nil, fmt.Errorf("failed to get admin address for node %s: %w", node.ID, err) - } - out[node.ID] = NodeOperator(dc.Name, a) - + for _, node := range dc.Nodes { + a, err := AdminAddress(&node, cs) + if err != nil { + return nil, fmt.Errorf("failed to get admin address for node %s: %w", node.ID, err) } + // TODO: this never mapped to nop name, but don name + out[node.ID] = NodeOperator(dc.Name, a) + } return out, nil } @@ -232,14 +234,15 @@ func NodeOperator(name string, adminAddress string) capabilities_registry.Capabi } } -func AdminAddress(n *models.Node, chainSel uint64) (string, error) { +func AdminAddress(n *Node, chainSel uint64) (string, error) { cid, err := chainsel.ChainIdFromSelector(chainSel) if err != nil { return "", fmt.Errorf("failed to get chain id from selector %d: %w", chainSel, err) } cidStr := strconv.FormatUint(cid, 10) for _, chain := range n.ChainConfigs { - if chain.Network.ChainID == cidStr { + //TODO validate chainType field + if chain.Chain.Id == cidStr { return chain.AdminAddress, nil } } @@ -248,7 +251,7 @@ func AdminAddress(n *models.Node, chainSel uint64) (string, error) { // helpers to maintain compatibility with the existing registration functions // nodesToNops converts a list of DonCapabilities to a map of node id to NOP -func nodesToNops(dons []DonCapabilities, chainSel uint64) (map[string]capabilities_registry.CapabilitiesRegistryNodeOperator, error) { +func nodesToNops(dons []DonInfo, chainSel uint64) (map[string]capabilities_registry.CapabilitiesRegistryNodeOperator, error) { out := make(map[string]capabilities_registry.CapabilitiesRegistryNodeOperator) for _, don := range dons { nops, err := don.nodeIdToNop(chainSel) @@ -267,7 +270,7 @@ func nodesToNops(dons []DonCapabilities, chainSel uint64) (map[string]capabiliti } // mapDonsToCaps converts a list of DonCapabilities to a map of don name to capabilities -func mapDonsToCaps(dons []DonCapabilities) map[string][]kcr.CapabilitiesRegistryCapability { +func mapDonsToCaps(dons []DonInfo) map[string][]kcr.CapabilitiesRegistryCapability { out := make(map[string][]kcr.CapabilitiesRegistryCapability) for _, don := range dons { out[don.Name] = don.Capabilities @@ -277,53 +280,48 @@ func mapDonsToCaps(dons []DonCapabilities) map[string][]kcr.CapabilitiesRegistry // mapDonsToNodes returns a map of don name to simplified representation of their nodes // all nodes must have evm config and ocr3 capability nodes are must also have an aptos chain config -func mapDonsToNodes(dons []DonCapabilities, excludeBootstraps bool, registryChainSel uint64) (map[string][]*ocr2Node, error) { +func mapDonsToNodes(dons []DonInfo, excludeBootstraps bool, registryChainSel uint64) (map[string][]*ocr2Node, error) { donToOcr2Nodes := make(map[string][]*ocr2Node) // get the nodes for each don from the offchain client, get ocr2 config from one of the chain configs for the node b/c // they are equivalent, and transform to ocr2node representation for _, don := range dons { - for _, nop := range don.Nops { - for _, node := range nop.Nodes { - ocr2n, err := newOcr2NodeFromClo(node, registryChainSel) - if err != nil { - return nil, fmt.Errorf("failed to create ocr2 node for node %s: %w", node.ID, err) - } - if excludeBootstraps && ocr2n.IsBoostrap { - continue - } - if _, ok := donToOcr2Nodes[don.Name]; !ok { - donToOcr2Nodes[don.Name] = make([]*ocr2Node, 0) - } - donToOcr2Nodes[don.Name] = append(donToOcr2Nodes[don.Name], ocr2n) - + for _, node := range don.Nodes { + ocr2n, err := newOcr2NodeFromClo(&node, registryChainSel) + if err != nil { + return nil, fmt.Errorf("failed to create ocr2 node for node %s: %w", node.ID, err) + } + if excludeBootstraps && ocr2n.IsBoostrap { + continue } + if _, ok := donToOcr2Nodes[don.Name]; !ok { + donToOcr2Nodes[don.Name] = make([]*ocr2Node, 0) + } + donToOcr2Nodes[don.Name] = append(donToOcr2Nodes[don.Name], ocr2n) } } return donToOcr2Nodes, nil } -func firstChainConfigByType(ccfgs []*models.NodeChainConfig, t chaintype.ChainType) (*v1.ChainConfig, bool) { +func firstChainConfigByType(ccfgs []*v1.ChainConfig, t v1.ChainType) (*v1.ChainConfig, bool) { for _, c := range ccfgs { - //nolint:staticcheck //ignore EqualFold it broke ci for some reason (go version skew btw local and ci?) - if strings.ToLower(c.Network.ChainType.String()) == strings.ToLower(string(t)) { - return chainConfigFromClo(c), true + if c.Chain.Type == t { + return c, true } } return nil, false } -func registryChainConfig(ccfgs []*models.NodeChainConfig, t chaintype.ChainType, sel uint64) (*v1.ChainConfig, error) { +func registryChainConfig(ccfgs []*v1.ChainConfig, t v1.ChainType, sel uint64) (*v1.ChainConfig, error) { chainId, err := chainsel.ChainIdFromSelector(sel) if err != nil { return nil, fmt.Errorf("failed to get chain id from selector %d: %w", sel, err) } chainIdStr := strconv.FormatUint(chainId, 10) for _, c := range ccfgs { - //nolint:staticcheck //ignore EqualFold it broke ci for some reason (go version skew btw local and ci?) - if strings.ToLower(c.Network.ChainType.String()) == strings.ToLower(string(t)) && c.Network.ChainID == chainIdStr { - return chainConfigFromClo(c), nil + if c.Chain.Type == t && c.Chain.Id == chainIdStr { + return c, nil } } return nil, fmt.Errorf("no chain config for chain %d", chainId) @@ -350,7 +348,7 @@ func (d RegisteredDon) signers() []common.Address { return out } -func joinInfoAndNodes(donInfos map[string]kcr.CapabilitiesRegistryDONInfo, dons []DonCapabilities, registryChainSel uint64) ([]RegisteredDon, error) { +func joinInfoAndNodes(donInfos map[string]kcr.CapabilitiesRegistryDONInfo, dons []DonInfo, registryChainSel uint64) ([]RegisteredDon, error) { // all maps should have the same keys nodes, err := mapDonsToNodes(dons, true, registryChainSel) if err != nil { @@ -376,31 +374,6 @@ func joinInfoAndNodes(donInfos map[string]kcr.CapabilitiesRegistryDONInfo, dons return out, nil } -func chainConfigFromClo(chain *models.NodeChainConfig) *v1.ChainConfig { - return &v1.ChainConfig{ - Chain: &v1.Chain{ - Id: chain.Network.ChainID, - Type: v1.ChainType_CHAIN_TYPE_EVM, // TODO: support other chain types - }, - - AccountAddress: chain.AccountAddress, - AdminAddress: chain.AdminAddress, - Ocr2Config: &v1.OCR2Config{ - Enabled: chain.Ocr2Config.Enabled, - P2PKeyBundle: &v1.OCR2Config_P2PKeyBundle{ - PeerId: chain.Ocr2Config.P2pKeyBundle.PeerID, - PublicKey: chain.Ocr2Config.P2pKeyBundle.PublicKey, - }, - OcrKeyBundle: &v1.OCR2Config_OCRKeyBundle{ - BundleId: chain.Ocr2Config.OcrKeyBundle.BundleID, - OnchainSigningAddress: chain.Ocr2Config.OcrKeyBundle.OnchainSigningAddress, - OffchainPublicKey: chain.Ocr2Config.OcrKeyBundle.OffchainPublicKey, - ConfigPublicKey: chain.Ocr2Config.OcrKeyBundle.ConfigPublicKey, - }, - }, - } -} - var emptyAddr = "0x0000000000000000000000000000000000000000" // compute the admin address from the string. If the address is empty, replaces the 0s with fs diff --git a/deployment/keystone/types_test.go b/deployment/keystone/types_test.go index 69b2e39a8f1..925649bba0d 100644 --- a/deployment/keystone/types_test.go +++ b/deployment/keystone/types_test.go @@ -1,19 +1,11 @@ package keystone import ( - "encoding/json" - "os" - "strconv" "testing" "github.com/stretchr/testify/assert" - "github.com/test-go/testify/require" - - chainsel "github.com/smartcontractkit/chain-selectors" v1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" - "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" - kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/chaintype" ) @@ -140,271 +132,271 @@ func Test_newOcr2Node(t *testing.T) { } } -func Test_mapDonsToNodes(t *testing.T) { - var ( - pubKey = "03dacd15fc96c965c648e3623180de002b71a97cf6eeca9affb91f461dcd6ce1" - evmSig = "b35409a8d4f9a18da55c5b2bb08a3f5f68d44442" - aptosSig = "b35409a8d4f9a18da55c5b2bb08a3f5f68d44442b35409a8d4f9a18da55c5b2bb08a3f5f68d44442" - peerID = "p2p_12D3KooWMWUKdoAc2ruZf9f55p7NVFj7AFiPm67xjQ8BZBwkqyYv" - // todo: these should be defined in common - writerCap = 3 - ocr3Cap = 2 - registryChainSel = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector - registryChainID = strconv.FormatUint(chainsel.ETHEREUM_TESTNET_SEPOLIA.EvmChainID, 10) - ) - type args struct { - dons []DonCapabilities - excludeBootstraps bool - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "writer evm only", - args: args{ - dons: []DonCapabilities{ - { - Name: "ok writer", - Nops: []*models.NodeOperator{ - { - Nodes: []*models.Node{ - { - PublicKey: &pubKey, - ChainConfigs: []*models.NodeChainConfig{ - { - ID: "1", - Network: &models.Network{ - ChainType: models.ChainTypeEvm, - ChainID: registryChainID, - }, - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: peerID, - }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ - ConfigPublicKey: pubKey, - OffchainPublicKey: pubKey, - OnchainSigningAddress: evmSig, - }, - }, - }, - }, - }, - }, - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "writer", - Version: "1", - CapabilityType: uint8(writerCap), - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "err if no evm chain", - args: args{ - dons: []DonCapabilities{ - { - Name: "bad chain", - Nops: []*models.NodeOperator{ - { - Nodes: []*models.Node{ - { - PublicKey: &pubKey, - ChainConfigs: []*models.NodeChainConfig{ - { - ID: "1", - Network: &models.Network{ - ChainType: models.ChainTypeSolana, - }, - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: peerID, - }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ - ConfigPublicKey: pubKey, - OffchainPublicKey: pubKey, - OnchainSigningAddress: evmSig, - }, - }, - }, - }, - }, - }, - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "writer", - Version: "1", - CapabilityType: uint8(writerCap), - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "ocr3 cap evm only", - args: args{ - dons: []DonCapabilities{ - { - Name: "bad chain", - Nops: []*models.NodeOperator{ - { - Nodes: []*models.Node{ - { - PublicKey: &pubKey, - ChainConfigs: []*models.NodeChainConfig{ - { - ID: "1", - Network: &models.Network{ - ChainType: models.ChainTypeEvm, - ChainID: registryChainID, - }, - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: peerID, - }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ - ConfigPublicKey: pubKey, - OffchainPublicKey: pubKey, - OnchainSigningAddress: evmSig, - }, - }, - }, - }, - }, - }, - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "ocr3", - Version: "1", - CapabilityType: uint8(ocr3Cap), - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "ocr3 cap evm & aptos", - args: args{ - dons: []DonCapabilities{ - { - Name: "ok chain", - Nops: []*models.NodeOperator{ - { - Nodes: []*models.Node{ - { - PublicKey: &pubKey, - ChainConfigs: []*models.NodeChainConfig{ - { - ID: "1", - Network: &models.Network{ - ChainType: models.ChainTypeEvm, - ChainID: registryChainID, - }, - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: peerID, - }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ - ConfigPublicKey: pubKey, - OffchainPublicKey: pubKey, - OnchainSigningAddress: evmSig, - }, - }, - }, - { - ID: "2", - Network: &models.Network{ - ChainType: models.ChainTypeAptos, - }, - Ocr2Config: &models.NodeOCR2Config{ - P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ - PeerID: peerID, - }, - OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ - ConfigPublicKey: pubKey, - OffchainPublicKey: pubKey, - OnchainSigningAddress: aptosSig, - }, - }, - }, - }, - }, - }, - }, - }, - Capabilities: []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "ocr3", - Version: "1", - CapabilityType: uint8(ocr3Cap), - }, - }, - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := mapDonsToNodes(tt.args.dons, tt.args.excludeBootstraps, registryChainSel) - if (err != nil) != tt.wantErr { - t.Errorf("mapDonsToNodes() error = %v, wantErr %v", err, tt.wantErr) - return - } - }) - } - // make sure the clo test data is correct - wfNops := loadTestNops(t, "testdata/workflow_nodes.json") - cwNops := loadTestNops(t, "testdata/chain_writer_nodes.json") - assetNops := loadTestNops(t, "testdata/asset_nodes.json") - require.Len(t, wfNops, 10) - require.Len(t, cwNops, 10) - require.Len(t, assetNops, 16) +// func Test_mapDonsToNodes(t *testing.T) { +// var ( +// pubKey = "03dacd15fc96c965c648e3623180de002b71a97cf6eeca9affb91f461dcd6ce1" +// evmSig = "b35409a8d4f9a18da55c5b2bb08a3f5f68d44442" +// aptosSig = "b35409a8d4f9a18da55c5b2bb08a3f5f68d44442b35409a8d4f9a18da55c5b2bb08a3f5f68d44442" +// peerID = "p2p_12D3KooWMWUKdoAc2ruZf9f55p7NVFj7AFiPm67xjQ8BZBwkqyYv" +// // todo: these should be defined in common +// writerCap = 3 +// ocr3Cap = 2 +// registryChainSel = chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector +// registryChainID = strconv.FormatUint(chainsel.ETHEREUM_TESTNET_SEPOLIA.EvmChainID, 10) +// ) +// type args struct { +// dons []DonCapabilities +// excludeBootstraps bool +// } +// tests := []struct { +// name string +// args args +// wantErr bool +// }{ +// { +// name: "writer evm only", +// args: args{ +// dons: []DonCapabilities{ +// { +// Name: "ok writer", +// Nops: []*models.NodeOperator{ +// { +// Nodes: []*models.Node{ +// { +// PublicKey: &pubKey, +// ChainConfigs: []*models.NodeChainConfig{ +// { +// ID: "1", +// Network: &models.Network{ +// ChainType: models.ChainTypeEvm, +// ChainID: registryChainID, +// }, +// Ocr2Config: &models.NodeOCR2Config{ +// P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ +// PeerID: peerID, +// }, +// OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ +// ConfigPublicKey: pubKey, +// OffchainPublicKey: pubKey, +// OnchainSigningAddress: evmSig, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// Capabilities: []kcr.CapabilitiesRegistryCapability{ +// { +// LabelledName: "writer", +// Version: "1", +// CapabilityType: uint8(writerCap), +// }, +// }, +// }, +// }, +// }, +// wantErr: false, +// }, +// { +// name: "err if no evm chain", +// args: args{ +// dons: []DonCapabilities{ +// { +// Name: "bad chain", +// Nops: []*models.NodeOperator{ +// { +// Nodes: []*models.Node{ +// { +// PublicKey: &pubKey, +// ChainConfigs: []*models.NodeChainConfig{ +// { +// ID: "1", +// Network: &models.Network{ +// ChainType: models.ChainTypeSolana, +// }, +// Ocr2Config: &models.NodeOCR2Config{ +// P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ +// PeerID: peerID, +// }, +// OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ +// ConfigPublicKey: pubKey, +// OffchainPublicKey: pubKey, +// OnchainSigningAddress: evmSig, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// Capabilities: []kcr.CapabilitiesRegistryCapability{ +// { +// LabelledName: "writer", +// Version: "1", +// CapabilityType: uint8(writerCap), +// }, +// }, +// }, +// }, +// }, +// wantErr: true, +// }, +// { +// name: "ocr3 cap evm only", +// args: args{ +// dons: []DonCapabilities{ +// { +// Name: "bad chain", +// Nops: []*models.NodeOperator{ +// { +// Nodes: []*models.Node{ +// { +// PublicKey: &pubKey, +// ChainConfigs: []*models.NodeChainConfig{ +// { +// ID: "1", +// Network: &models.Network{ +// ChainType: models.ChainTypeEvm, +// ChainID: registryChainID, +// }, +// Ocr2Config: &models.NodeOCR2Config{ +// P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ +// PeerID: peerID, +// }, +// OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ +// ConfigPublicKey: pubKey, +// OffchainPublicKey: pubKey, +// OnchainSigningAddress: evmSig, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// Capabilities: []kcr.CapabilitiesRegistryCapability{ +// { +// LabelledName: "ocr3", +// Version: "1", +// CapabilityType: uint8(ocr3Cap), +// }, +// }, +// }, +// }, +// }, +// wantErr: false, +// }, +// { +// name: "ocr3 cap evm & aptos", +// args: args{ +// dons: []DonCapabilities{ +// { +// Name: "ok chain", +// Nops: []*models.NodeOperator{ +// { +// Nodes: []*models.Node{ +// { +// PublicKey: &pubKey, +// ChainConfigs: []*models.NodeChainConfig{ +// { +// ID: "1", +// Network: &models.Network{ +// ChainType: models.ChainTypeEvm, +// ChainID: registryChainID, +// }, +// Ocr2Config: &models.NodeOCR2Config{ +// P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ +// PeerID: peerID, +// }, +// OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ +// ConfigPublicKey: pubKey, +// OffchainPublicKey: pubKey, +// OnchainSigningAddress: evmSig, +// }, +// }, +// }, +// { +// ID: "2", +// Network: &models.Network{ +// ChainType: models.ChainTypeAptos, +// }, +// Ocr2Config: &models.NodeOCR2Config{ +// P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ +// PeerID: peerID, +// }, +// OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ +// ConfigPublicKey: pubKey, +// OffchainPublicKey: pubKey, +// OnchainSigningAddress: aptosSig, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// Capabilities: []kcr.CapabilitiesRegistryCapability{ +// { +// LabelledName: "ocr3", +// Version: "1", +// CapabilityType: uint8(ocr3Cap), +// }, +// }, +// }, +// }, +// }, +// wantErr: false, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// _, err := mapDonsToNodes(tt.args.dons, tt.args.excludeBootstraps, registryChainSel) +// if (err != nil) != tt.wantErr { +// t.Errorf("mapDonsToNodes() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// }) +// } +// // make sure the clo test data is correct +// wfNops := loadTestNops(t, "testdata/workflow_nodes.json") +// cwNops := loadTestNops(t, "testdata/chain_writer_nodes.json") +// assetNops := loadTestNops(t, "testdata/asset_nodes.json") +// require.Len(t, wfNops, 10) +// require.Len(t, cwNops, 10) +// require.Len(t, assetNops, 16) - wfDon := DonCapabilities{ - Name: WFDonName, - Nops: wfNops, - Capabilities: []kcr.CapabilitiesRegistryCapability{OCR3Cap}, - } - cwDon := DonCapabilities{ - Name: TargetDonName, - Nops: cwNops, - Capabilities: []kcr.CapabilitiesRegistryCapability{WriteChainCap}, - } - assetDon := DonCapabilities{ - Name: StreamDonName, - Nops: assetNops, - Capabilities: []kcr.CapabilitiesRegistryCapability{StreamTriggerCap}, - } - _, err := mapDonsToNodes([]DonCapabilities{wfDon}, false, registryChainSel) - require.NoError(t, err, "failed to map wf don") - _, err = mapDonsToNodes([]DonCapabilities{cwDon}, false, registryChainSel) - require.NoError(t, err, "failed to map cw don") - _, err = mapDonsToNodes([]DonCapabilities{assetDon}, false, registryChainSel) - require.NoError(t, err, "failed to map asset don") -} +// wfDon := DonCapabilities{ +// Name: WFDonName, +// Nops: wfNops, +// Capabilities: []kcr.CapabilitiesRegistryCapability{OCR3Cap}, +// } +// cwDon := DonCapabilities{ +// Name: TargetDonName, +// Nops: cwNops, +// Capabilities: []kcr.CapabilitiesRegistryCapability{WriteChainCap}, +// } +// assetDon := DonCapabilities{ +// Name: StreamDonName, +// Nops: assetNops, +// Capabilities: []kcr.CapabilitiesRegistryCapability{StreamTriggerCap}, +// } +// _, err := mapDonsToNodes([]DonCapabilities{wfDon}, false, registryChainSel) +// require.NoError(t, err, "failed to map wf don") +// _, err = mapDonsToNodes([]DonCapabilities{cwDon}, false, registryChainSel) +// require.NoError(t, err, "failed to map cw don") +// _, err = mapDonsToNodes([]DonCapabilities{assetDon}, false, registryChainSel) +// require.NoError(t, err, "failed to map asset don") +// } -func loadTestNops(t *testing.T, pth string) []*models.NodeOperator { - f, err := os.ReadFile(pth) - require.NoError(t, err) - var nops []*models.NodeOperator - require.NoError(t, json.Unmarshal(f, &nops)) - return nops -} +// func loadTestNops(t *testing.T, pth string) []*models.NodeOperator { +// f, err := os.ReadFile(pth) +// require.NoError(t, err) +// var nops []*models.NodeOperator +// require.NoError(t, json.Unmarshal(f, &nops)) +// return nops +// } diff --git a/shell.nix b/shell.nix index e3b187dcd96..8d5b4351b25 100644 --- a/shell.nix +++ b/shell.nix @@ -1,7 +1,7 @@ {pkgs, isCrib}: with pkgs; let go = go_1_21; - postgresql = postgresql_14; + postgresql = postgresql_15; nodejs = nodejs-18_x; nodePackages = pkgs.nodePackages.override {inherit nodejs;}; pnpm = pnpm_9;