diff --git a/integration-tests/deployment/keystone/capability_management.go b/integration-tests/deployment/keystone/capability_management.go new file mode 100644 index 00000000000..20b07727510 --- /dev/null +++ b/integration-tests/deployment/keystone/capability_management.go @@ -0,0 +1,64 @@ +package keystone + +import ( + "fmt" + "strings" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +// AddCapabilities adds the capabilities to the registry +// it tries to add all capabilities in one go, if that fails, it falls back to adding them one by one +func AddCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, chain deployment.Chain, capabilities []kcr.CapabilitiesRegistryCapability) error { + if len(capabilities) == 0 { + return nil + } + // dedup capabilities + var deduped []kcr.CapabilitiesRegistryCapability + seen := make(map[string]struct{}) + for _, cap := range capabilities { + if _, ok := seen[CapabilityID(cap)]; !ok { + seen[CapabilityID(cap)] = struct{}{} + deduped = append(deduped, cap) + } + } + + tx, err := registry.AddCapabilities(chain.DeployerKey, deduped) + if err != nil { + err = DecodeErr(kcr.CapabilitiesRegistryABI, err) + // no typed errors in the abi, so we have to do string matching + // try to add all capabilities in one go, if that fails, fall back to 1-by-1 + if !strings.Contains(err.Error(), "CapabilityAlreadyExists") { + return fmt.Errorf("failed to call AddCapabilities: %w", err) + } + lggr.Warnw("capabilities already exist, falling back to 1-by-1", "capabilities", deduped) + for _, cap := range deduped { + tx, err = registry.AddCapabilities(chain.DeployerKey, []kcr.CapabilitiesRegistryCapability{cap}) + if err != nil { + err = DecodeErr(kcr.CapabilitiesRegistryABI, err) + if strings.Contains(err.Error(), "CapabilityAlreadyExists") { + lggr.Warnw("capability already exists, skipping", "capability", cap) + continue + } + return fmt.Errorf("failed to call AddCapabilities for capability %v: %w", cap, err) + } + // 1-by-1 tx is pending and we need to wait for it to be mined + _, err = chain.Confirm(tx) + if err != nil { + return fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) + } + lggr.Debugw("registered capability", "capability", cap) + + } + } else { + // the bulk add tx is pending and we need to wait for it to be mined + _, err = chain.Confirm(tx) + if err != nil { + return fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) + } + lggr.Info("registered capabilities", "capabilities", deduped) + } + return nil +} diff --git a/integration-tests/deployment/keystone/capability_registry_deployer.go b/integration-tests/deployment/keystone/capability_registry_deployer.go index cd4de63558c..3c65c08e3fb 100644 --- a/integration-tests/deployment/keystone/capability_registry_deployer.go +++ b/integration-tests/deployment/keystone/capability_registry_deployer.go @@ -17,12 +17,20 @@ type CapabilitiesRegistryDeployer struct { contract *capabilities_registry.CapabilitiesRegistry } +func NewCapabilitiesRegistryDeployer(lggr logger.Logger) *CapabilitiesRegistryDeployer { + return &CapabilitiesRegistryDeployer{lggr: lggr} +} + +func (c *CapabilitiesRegistryDeployer) Contract() *capabilities_registry.CapabilitiesRegistry { + return c.contract +} + var CapabilityRegistryTypeVersion = deployment.TypeAndVersion{ Type: CapabilitiesRegistry, Version: deployment.Version1_0_0, } -func (c *CapabilitiesRegistryDeployer) deploy(req deployRequest) (*deployResponse, error) { +func (c *CapabilitiesRegistryDeployer) Deploy(req DeployRequest) (*DeployResponse, error) { est, err := estimateDeploymentGas(req.Chain.Client, capabilities_registry.CapabilitiesRegistryABI) if err != nil { return nil, fmt.Errorf("failed to estimate gas: %w", err) @@ -40,7 +48,7 @@ func (c *CapabilitiesRegistryDeployer) deploy(req deployRequest) (*deployRespons if err != nil { return nil, fmt.Errorf("failed to confirm and save CapabilitiesRegistry: %w", err) } - resp := &deployResponse{ + resp := &DeployResponse{ Address: capabilitiesRegistryAddr, Tx: tx.Hash(), Tv: CapabilityRegistryTypeVersion, diff --git a/integration-tests/deployment/keystone/changeset/append_node_capabilities_test.go b/integration-tests/deployment/keystone/changeset/append_node_capabilities_test.go new file mode 100644 index 00000000000..5026321f289 --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/append_node_capabilities_test.go @@ -0,0 +1,135 @@ +package changeset_test + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" + "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/changeset" + kstest "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/test" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func TestAppendNodeCapabilities(t *testing.T) { + var ( + initialp2pToCapabilities = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "0x1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "test", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "testNop"): []*kslib.P2PSignerEnc{ + &kslib.P2PSignerEnc{ + Signer: [32]byte{0: 1}, + P2PKey: testPeerID(t, "0x1"), + EncryptionPublicKey: [32]byte{7: 7, 13: 13}, + }, + }, + } + ) + + lggr := logger.Test(t) + + type args struct { + lggr logger.Logger + req *changeset.AppendNodeCapabilitiesRequest + initialState *kstest.SetupTestRegistryRequest + } + tests := []struct { + name string + args args + want deployment.ChangesetOutput + wantErr bool + }{ + { + name: "invalid request", + args: args{ + lggr: lggr, + req: &changeset.AppendNodeCapabilitiesRequest{ + Chain: deployment.Chain{}, + }, + initialState: &kstest.SetupTestRegistryRequest{}, + }, + wantErr: true, + }, + { + name: "happy path", + args: args{ + lggr: lggr, + initialState: &kstest.SetupTestRegistryRequest{ + P2pToCapabilities: initialp2pToCapabilities, + NopToNodes: nopToNodes, + }, + req: &changeset.AppendNodeCapabilitiesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "0x1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.0", + CapabilityType: 0, + }, + { + LabelledName: "cap3", + Version: "1.0.0", + CapabilityType: 3, + }, + }, + }, + NopToNodes: nopToNodes, + }, + }, + want: deployment.ChangesetOutput{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // chagen the name and args to be mor egeneral + setupResp := kstest.SetupTestRegistry(t, lggr, tt.args.initialState) + + tt.args.req.Registry = setupResp.Registry + tt.args.req.Chain = setupResp.Chain + + got, err := changeset.AppendNodeCapabilitiesImpl(tt.args.lggr, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("AppendNodeCapabilities() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + return + } + require.NotNil(t, got) + // should be one node param for each input p2p id + assert.Len(t, got.NodeParams, len(tt.args.req.P2pToCapabilities)) + for _, nodeParam := range got.NodeParams { + initialCapsOnNode := tt.args.initialState.P2pToCapabilities[nodeParam.P2pId] + appendCaps := tt.args.req.P2pToCapabilities[nodeParam.P2pId] + assert.Len(t, nodeParam.HashedCapabilityIds, len(initialCapsOnNode)+len(appendCaps)) + } + }) + } +} + +func testPeerID(t *testing.T, s string) p2pkey.PeerID { + var out [32]byte + b := []byte(s) + copy(out[:], b) + return p2pkey.PeerID(out) +} + +func testNop(t *testing.T, name string) kcr.CapabilitiesRegistryNodeOperator { + return kcr.CapabilitiesRegistryNodeOperator{ + Admin: common.HexToAddress("0xFFFFFFFF45297A703e4508186d4C1aa1BAf80000"), + Name: name, + } +} diff --git a/integration-tests/deployment/keystone/changeset/append_node_capbilities.go b/integration-tests/deployment/keystone/changeset/append_node_capbilities.go new file mode 100644 index 00000000000..0034050c6cd --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/append_node_capbilities.go @@ -0,0 +1,80 @@ +package changeset + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" +) + +type AppendNodeCapabilitiesRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + + P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc +} + +func (req *AppendNodeCapabilitiesRequest) Validate() error { + if len(req.P2pToCapabilities) == 0 { + return fmt.Errorf("p2pToCapabilities is empty") + } + if len(req.NopToNodes) == 0 { + return fmt.Errorf("nopToNodes is empty") + } + if req.Registry == nil { + return fmt.Errorf("registry is nil") + } + return nil +} + +// AppendNodeCapabilibity adds any new capabilities to the registry, merges the new capabilities with the existing capabilities +// of the node, and updates the nodes in the registry host the union of the new and existing capabilities. +func AppendNodeCapabilities(lggr logger.Logger, req *AppendNodeCapabilitiesRequest) (deployment.ChangesetOutput, error) { + _, err := appendNodeCapabilitiesImpl(lggr, req) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{}, nil +} + +func appendNodeCapabilitiesImpl(lggr logger.Logger, req *AppendNodeCapabilitiesRequest) (*kslib.UpdateNodesResponse, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate request: %w", err) + } + // collect all the capabilities and add them to the registry + var capabilities []kcr.CapabilitiesRegistryCapability + for _, cap := range req.P2pToCapabilities { + capabilities = append(capabilities, cap...) + } + err := kslib.AddCapabilities(lggr, req.Registry, req.Chain, capabilities) + if err != nil { + return nil, fmt.Errorf("failed to add capabilities: %w", err) + } + + // for each node, merge the new capabilities with the existing ones and update the node + capsByPeer := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for p2pID, caps := range req.P2pToCapabilities { + caps, err := kslib.AppendCapabilities(lggr, req.Registry, req.Chain, []p2pkey.PeerID{p2pID}, caps) + if err != nil { + return nil, fmt.Errorf("failed to append capabilities for p2p %s: %w", p2pID, err) + } + capsByPeer[p2pID] = caps[p2pID] + } + + updateNodesReq := &kslib.UpdateNodesRequest{ + Chain: req.Chain, + Registry: req.Registry, + P2pToCapabilities: capsByPeer, + NopToNodes: req.NopToNodes, + } + resp, err := kslib.UpdateNodes(lggr, updateNodesReq) + if err != nil { + return nil, fmt.Errorf("failed to update nodes: %w", err) + } + return resp, nil +} diff --git a/integration-tests/deployment/keystone/changeset/helpers_test.go b/integration-tests/deployment/keystone/changeset/helpers_test.go new file mode 100644 index 00000000000..c15b59fc400 --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/helpers_test.go @@ -0,0 +1,7 @@ +package changeset + +// AppendNodeCapabilitiesImpl exported so we can test the onchain result of the AppendNodeCapability Changeset function +var AppendNodeCapabilitiesImpl = appendNodeCapabilitiesImpl + +// UpdateNodeCapabilitiesImpl exported so we can test the onchain result of UpdateNodeCapability Changeset function +var UpdateNodeCapabilitiesImpl = updateNodeCapabilitiesImpl diff --git a/integration-tests/deployment/keystone/changeset/update_node_capabilities.go b/integration-tests/deployment/keystone/changeset/update_node_capabilities.go new file mode 100644 index 00000000000..2a5a0d7e01d --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/update_node_capabilities.go @@ -0,0 +1,69 @@ +package changeset + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +type UpdateNodeCapabilitiesRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + + P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc +} + +func (req *UpdateNodeCapabilitiesRequest) Validate() error { + if len(req.P2pToCapabilities) == 0 { + return fmt.Errorf("p2pToCapabilities is empty") + } + if len(req.NopToNodes) == 0 { + return fmt.Errorf("nopToNodes is empty") + } + if req.Registry == nil { + return fmt.Errorf("registry is nil") + } + return nil +} + +// UpdateNodeCapabilibity sets the capabilities of the node to the new capabilities. +// New capabilities are added to the onchain registry and the node is updated to host the new capabilities. +func UpdateNodeCapabilities(lggr logger.Logger, req *UpdateNodeCapabilitiesRequest) (deployment.ChangesetOutput, error) { + _, err := updateNodeCapabilitiesImpl(lggr, req) + if err != nil { + return deployment.ChangesetOutput{}, err + } + return deployment.ChangesetOutput{}, nil +} + +func updateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesRequest) (*kslib.UpdateNodesResponse, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate request: %w", err) + } + // collect all the capabilities and add them to the registry + var capabilities []kcr.CapabilitiesRegistryCapability + for _, cap := range req.P2pToCapabilities { + capabilities = append(capabilities, cap...) + } + err := kslib.AddCapabilities(lggr, req.Registry, req.Chain, capabilities) + if err != nil { + return nil, fmt.Errorf("failed to add capabilities: %w", err) + } + + updateNodesReq := &kslib.UpdateNodesRequest{ + Chain: req.Chain, + Registry: req.Registry, + P2pToCapabilities: req.P2pToCapabilities, + NopToNodes: req.NopToNodes, + } + resp, err := kslib.UpdateNodes(lggr, updateNodesReq) + if err != nil { + return nil, fmt.Errorf("failed to update nodes: %w", err) + } + return resp, nil +} diff --git a/integration-tests/deployment/keystone/changeset/update_node_capabilities_test.go b/integration-tests/deployment/keystone/changeset/update_node_capabilities_test.go new file mode 100644 index 00000000000..4857b36b4da --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/update_node_capabilities_test.go @@ -0,0 +1,119 @@ +package changeset_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" + "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/changeset" + kstest "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/test" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func TestUpdateNodeCapabilities(t *testing.T) { + var ( + initialp2pToCapabilities = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "0x1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "test", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "testNop"): []*kslib.P2PSignerEnc{ + &kslib.P2PSignerEnc{ + Signer: [32]byte{0: 1}, + P2PKey: testPeerID(t, "0x1"), + EncryptionPublicKey: [32]byte{3: 16, 4: 2}, + }, + }, + } + ) + + lggr := logger.Test(t) + + type args struct { + lggr logger.Logger + req *changeset.UpdateNodeCapabilitiesRequest + initialState *kstest.SetupTestRegistryRequest + } + tests := []struct { + name string + args args + want deployment.ChangesetOutput + wantErr bool + }{ + { + name: "invalid request", + args: args{ + lggr: lggr, + req: &changeset.UpdateNodeCapabilitiesRequest{ + Chain: deployment.Chain{}, + }, + initialState: &kstest.SetupTestRegistryRequest{}, + }, + wantErr: true, + }, + { + name: "happy path", + args: args{ + lggr: lggr, + initialState: &kstest.SetupTestRegistryRequest{ + P2pToCapabilities: initialp2pToCapabilities, + NopToNodes: nopToNodes, + }, + req: &changeset.UpdateNodeCapabilitiesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "0x1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.0", + CapabilityType: 0, + }, + { + LabelledName: "cap3", + Version: "1.0.0", + CapabilityType: 3, + }, + }, + }, + NopToNodes: nopToNodes, + }, + }, + want: deployment.ChangesetOutput{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // chagen the name and args to be mor egeneral + setupResp := kstest.SetupTestRegistry(t, lggr, tt.args.initialState) + + tt.args.req.Registry = setupResp.Registry + tt.args.req.Chain = setupResp.Chain + + got, err := changeset.UpdateNodeCapabilitiesImpl(tt.args.lggr, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("UpdateNodeCapabilities() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + return + } + require.NotNil(t, got) + // update is a set, so there should be 2 capabilities on the node + assert.Len(t, got.NodeParams, len(tt.args.req.P2pToCapabilities)) + for _, nodeParam := range got.NodeParams { + setCaps := tt.args.req.P2pToCapabilities[nodeParam.P2pId] + assert.Len(t, nodeParam.HashedCapabilityIds, len(setCaps)) + } + }) + } +} diff --git a/integration-tests/deployment/keystone/contract_set.go b/integration-tests/deployment/keystone/contract_set.go index d82532614ae..06bbc9acd4a 100644 --- a/integration-tests/deployment/keystone/contract_set.go +++ b/integration-tests/deployment/keystone/contract_set.go @@ -48,7 +48,7 @@ func deployContractsToChain(lggr logger.Logger, req deployContractsRequest) (*de // and saves the address in the address book. This mutates the address book. func DeployCapabilitiesRegistry(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { capabilitiesRegistryDeployer := CapabilitiesRegistryDeployer{lggr: lggr} - capabilitiesRegistryResp, err := capabilitiesRegistryDeployer.deploy(deployRequest{Chain: chain}) + capabilitiesRegistryResp, err := capabilitiesRegistryDeployer.Deploy(DeployRequest{Chain: chain}) if err != nil { return fmt.Errorf("failed to deploy CapabilitiesRegistry: %w", err) } @@ -64,7 +64,7 @@ func DeployCapabilitiesRegistry(lggr logger.Logger, chain deployment.Chain, ab d // and saves the address in the address book. This mutates the address book. func DeployOCR3(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { ocr3Deployer := OCR3Deployer{lggr: lggr} - ocr3Resp, err := ocr3Deployer.deploy(deployRequest{Chain: chain}) + ocr3Resp, err := ocr3Deployer.deploy(DeployRequest{Chain: chain}) if err != nil { return fmt.Errorf("failed to deploy OCR3Capability: %w", err) } @@ -80,7 +80,7 @@ func DeployOCR3(lggr logger.Logger, chain deployment.Chain, ab deployment.Addres // and saves the address in the address book. This mutates the address book. func DeployForwarder(lggr logger.Logger, chain deployment.Chain, ab deployment.AddressBook) error { forwarderDeployer := KeystoneForwarderDeployer{lggr: lggr} - forwarderResp, err := forwarderDeployer.deploy(deployRequest{Chain: chain}) + forwarderResp, err := forwarderDeployer.deploy(DeployRequest{Chain: chain}) if err != nil { return fmt.Errorf("failed to deploy KeystoneForwarder: %w", err) } diff --git a/integration-tests/deployment/keystone/deploy.go b/integration-tests/deployment/keystone/deploy.go index 71babbb2180..dce7c949729 100644 --- a/integration-tests/deployment/keystone/deploy.go +++ b/integration-tests/deployment/keystone/deploy.go @@ -65,7 +65,7 @@ func (r ConfigureContractsRequest) Validate() error { type ConfigureContractsResponse struct { Changeset *deployment.ChangesetOutput - DonInfos map[string]capabilities_registry.CapabilitiesRegistryDONInfo + DonInfos map[string]kcr.CapabilitiesRegistryDONInfo } // ConfigureContracts configures contracts them with the given DONS and their capabilities. It optionally deploys the contracts @@ -158,7 +158,7 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon } // ensure registry is deployed and get the registry contract and chain - var registry *capabilities_registry.CapabilitiesRegistry + var registry *kcr.CapabilitiesRegistry registryChainContracts, ok := contractSetsResp.ContractSets[req.RegistryChainSel] if !ok { return nil, fmt.Errorf("failed to deploy registry chain contracts. expected chain %d", req.RegistryChainSel) @@ -196,19 +196,19 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon lggr.Infow("registered capabilities", "capabilities", capabilitiesResp.donToCapabilities) // register node operators - var nops []capabilities_registry.CapabilitiesRegistryNodeOperator + var nops []kcr.CapabilitiesRegistryNodeOperator for _, nop := range nodeIdToNop { nops = append(nops, nop) } - nopsResp, err := registerNOPS(ctx, registerNOPSRequest{ - chain: registryChain, - registry: registry, - nops: nops, + nopsResp, err := RegisterNOPS(ctx, RegisterNOPSRequest{ + Chain: registryChain, + Registry: registry, + Nops: nops, }) if err != nil { return nil, fmt.Errorf("failed to register node operators: %w", err) } - lggr.Infow("registered node operators", "nops", nopsResp.nops) + lggr.Infow("registered node operators", "nops", nopsResp.Nops) // register nodes nodesResp, err := registerNodes(lggr, ®isterNodesRequest{ @@ -217,7 +217,7 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon nodeIdToNop: nodeIdToNop, donToOcr2Nodes: donToOcr2Nodes, donToCapabilities: capabilitiesResp.donToCapabilities, - nops: nopsResp.nops, + nops: nopsResp.Nops, }) if err != nil { return nil, fmt.Errorf("failed to register nodes: %w", err) @@ -357,17 +357,17 @@ func ConfigureOCR3ContractFromCLO(env *deployment.Environment, chainSel uint64, type registerCapabilitiesRequest struct { chain deployment.Chain - registry *capabilities_registry.CapabilitiesRegistry + registry *kcr.CapabilitiesRegistry donToCapabilities map[string][]kcr.CapabilitiesRegistryCapability } type registerCapabilitiesResponse struct { - donToCapabilities map[string][]registeredCapability + donToCapabilities map[string][]RegisteredCapability } -type registeredCapability struct { - capabilities_registry.CapabilitiesRegistryCapability - id [32]byte +type RegisteredCapability struct { + kcr.CapabilitiesRegistryCapability + ID [32]byte } // registerCapabilities add computes the capability id, adds it to the registry and associates the registered capabilities with appropriate don(s) @@ -376,13 +376,13 @@ func registerCapabilities(lggr logger.Logger, req registerCapabilitiesRequest) ( return nil, fmt.Errorf("no capabilities to register") } resp := ®isterCapabilitiesResponse{ - donToCapabilities: make(map[string][]registeredCapability), + donToCapabilities: make(map[string][]RegisteredCapability), } // capability could be hosted on multiple dons. need to deduplicate uniqueCaps := make(map[kcr.CapabilitiesRegistryCapability][32]byte) for don, caps := range req.donToCapabilities { - var registerCaps []registeredCapability + var registerCaps []RegisteredCapability for _, cap := range caps { id, ok := uniqueCaps[cap] if !ok { @@ -393,9 +393,9 @@ func registerCapabilities(lggr logger.Logger, req registerCapabilitiesRequest) ( } uniqueCaps[cap] = id } - registerCap := registeredCapability{ + registerCap := RegisteredCapability{ CapabilitiesRegistryCapability: cap, - id: id, + ID: id, } lggr.Debugw("hashed capability id", "capability", cap, "id", id) registerCaps = append(registerCaps, registerCap) @@ -408,84 +408,53 @@ func registerCapabilities(lggr logger.Logger, req registerCapabilitiesRequest) ( capabilities = append(capabilities, cap) } - tx, err := req.registry.AddCapabilities(req.chain.DeployerKey, capabilities) + err := AddCapabilities(lggr, req.registry, req.chain, capabilities) if err != nil { - err = DecodeErr(kcr.CapabilitiesRegistryABI, err) - // no typed errors in the abi, so we have to do string matching - // try to add all capabilities in one go, if that fails, fall back to 1-by-1 - if !strings.Contains(err.Error(), "CapabilityAlreadyExists") { - return nil, fmt.Errorf("failed to call AddCapabilities: %w", err) - } - lggr.Warnw("capabilities already exist, falling back to 1-by-1", "capabilities", capabilities) - for _, cap := range capabilities { - tx, err = req.registry.AddCapabilities(req.chain.DeployerKey, []kcr.CapabilitiesRegistryCapability{cap}) - if err != nil { - err = DecodeErr(kcr.CapabilitiesRegistryABI, err) - if strings.Contains(err.Error(), "CapabilityAlreadyExists") { - lggr.Warnw("capability already exists, skipping", "capability", cap) - continue - } - return nil, fmt.Errorf("failed to call AddCapabilities for capability %v: %w", cap, err) - } - // 1-by-1 tx is pending and we need to wait for it to be mined - _, err = req.chain.Confirm(tx) - if err != nil { - return nil, fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) - } - lggr.Debugw("registered capability", "capability", cap) - - } - } else { - // the bulk add tx is pending and we need to wait for it to be mined - _, err = req.chain.Confirm(tx) - if err != nil { - return nil, fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) - } - lggr.Info("registered capabilities", "capabilities", capabilities) + return nil, fmt.Errorf("failed to add capabilities: %w", err) } return resp, nil } -type registerNOPSRequest struct { - chain deployment.Chain - registry *capabilities_registry.CapabilitiesRegistry - nops []capabilities_registry.CapabilitiesRegistryNodeOperator +type RegisterNOPSRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + Nops []kcr.CapabilitiesRegistryNodeOperator } -type registerNOPSResponse struct { - nops []*capabilities_registry.CapabilitiesRegistryNodeOperatorAdded +type RegisterNOPSResponse struct { + Nops []*kcr.CapabilitiesRegistryNodeOperatorAdded } -func registerNOPS(ctx context.Context, req registerNOPSRequest) (*registerNOPSResponse, error) { - nops := req.nops - tx, err := req.registry.AddNodeOperators(req.chain.DeployerKey, nops) +func RegisterNOPS(ctx context.Context, req RegisterNOPSRequest) (*RegisterNOPSResponse, error) { + nops := req.Nops + tx, err := req.Registry.AddNodeOperators(req.Chain.DeployerKey, nops) if err != nil { err = DecodeErr(kcr.CapabilitiesRegistryABI, err) return nil, fmt.Errorf("failed to call AddNodeOperators: %w", err) } // for some reason that i don't understand, the confirm must be called before the WaitMined or the latter will hang // (at least for a simulated backend chain) - _, err = req.chain.Confirm(tx) + _, err = req.Chain.Confirm(tx) if err != nil { return nil, fmt.Errorf("failed to confirm AddNodeOperators confirm transaction %s: %w", tx.Hash().String(), err) } - receipt, err := bind.WaitMined(ctx, req.chain.Client, tx) + receipt, err := bind.WaitMined(ctx, req.Chain.Client, tx) if err != nil { return nil, fmt.Errorf("failed to mine AddNodeOperators confirm transaction %s: %w", tx.Hash().String(), err) } if len(receipt.Logs) != len(nops) { return nil, fmt.Errorf("expected %d log entries for AddNodeOperators, got %d", len(nops), len(receipt.Logs)) } - resp := ®isterNOPSResponse{ - nops: make([]*capabilities_registry.CapabilitiesRegistryNodeOperatorAdded, len(receipt.Logs)), + resp := &RegisterNOPSResponse{ + Nops: make([]*kcr.CapabilitiesRegistryNodeOperatorAdded, len(receipt.Logs)), } for i, log := range receipt.Logs { - o, err := req.registry.ParseNodeOperatorAdded(*log) + o, err := req.Registry.ParseNodeOperatorAdded(*log) if err != nil { return nil, fmt.Errorf("failed to parse log %d for operator added: %w", i, err) } - resp.nops[i] = o + resp.Nops[i] = o } return resp, nil @@ -551,31 +520,31 @@ func DecodeErr(encodedABI string, err error) error { // register nodes type registerNodesRequest struct { - registry *capabilities_registry.CapabilitiesRegistry + registry *kcr.CapabilitiesRegistry chain deployment.Chain - nodeIdToNop map[string]capabilities_registry.CapabilitiesRegistryNodeOperator + nodeIdToNop map[string]kcr.CapabilitiesRegistryNodeOperator donToOcr2Nodes map[string][]*ocr2Node - donToCapabilities map[string][]registeredCapability - nops []*capabilities_registry.CapabilitiesRegistryNodeOperatorAdded + donToCapabilities map[string][]RegisteredCapability + nops []*kcr.CapabilitiesRegistryNodeOperatorAdded } type registerNodesResponse struct { - nodeIDToParams map[string]capabilities_registry.CapabilitiesRegistryNodeParams + nodeIDToParams map[string]kcr.CapabilitiesRegistryNodeParams } // registerNodes registers the nodes with the registry. it assumes that the deployer key in the Chain // can sign the transactions update the contract state // TODO: 467 refactor to support MCMS. Specifically need to separate the call data generation from the actual contract call func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNodesResponse, error) { - nopToNodeIDs := make(map[capabilities_registry.CapabilitiesRegistryNodeOperator][]string) + nopToNodeIDs := make(map[kcr.CapabilitiesRegistryNodeOperator][]string) for nodeID, nop := range req.nodeIdToNop { if _, ok := nopToNodeIDs[nop]; !ok { nopToNodeIDs[nop] = make([]string, 0) } nopToNodeIDs[nop] = append(nopToNodeIDs[nop], nodeID) } - nodeToRegisterNop := make(map[string]*capabilities_registry.CapabilitiesRegistryNodeOperatorAdded) + nodeToRegisterNop := make(map[string]*kcr.CapabilitiesRegistryNodeOperatorAdded) for _, nop := range req.nops { - n := capabilities_registry.CapabilitiesRegistryNodeOperator{ + n := kcr.CapabilitiesRegistryNodeOperator{ Name: nop.Name, Admin: nop.Admin, } @@ -588,7 +557,7 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode } } - nodeIDToParams := make(map[string]capabilities_registry.CapabilitiesRegistryNodeParams) + nodeIDToParams := make(map[string]kcr.CapabilitiesRegistryNodeParams) for don, ocr2nodes := range req.donToOcr2Nodes { caps, ok := req.donToCapabilities[don] if !ok { @@ -596,7 +565,7 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode } var hashedCapabilityIds [][32]byte for _, cap := range caps { - hashedCapabilityIds = append(hashedCapabilityIds, cap.id) + hashedCapabilityIds = append(hashedCapabilityIds, cap.ID) } lggr.Debugw("hashed capability ids", "don", don, "ids", hashedCapabilityIds) @@ -611,7 +580,7 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode params, ok := nodeIDToParams[n.ID] if !ok { - params = capabilities_registry.CapabilitiesRegistryNodeParams{ + params = kcr.CapabilitiesRegistryNodeParams{ NodeOperatorId: nop.NodeOperatorId, Signer: n.Signer, P2pId: n.P2PKey, @@ -639,7 +608,7 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode } } - var uniqueNodeParams []capabilities_registry.CapabilitiesRegistryNodeParams + var uniqueNodeParams []kcr.CapabilitiesRegistryNodeParams for _, v := range nodeIDToParams { uniqueNodeParams = append(uniqueNodeParams, v) } @@ -654,7 +623,7 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode } lggr.Warn("nodes already exist, falling back to 1-by-1") for _, singleNodeParams := range uniqueNodeParams { - tx, err = req.registry.AddNodes(req.chain.DeployerKey, []capabilities_registry.CapabilitiesRegistryNodeParams{singleNodeParams}) + tx, err = req.registry.AddNodes(req.chain.DeployerKey, []kcr.CapabilitiesRegistryNodeParams{singleNodeParams}) if err != nil { err = DecodeErr(kcr.CapabilitiesRegistryABI, err) if strings.Contains(err.Error(), "NodeAlreadyExists") { @@ -683,16 +652,16 @@ func registerNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNode } type registerDonsRequest struct { - registry *capabilities_registry.CapabilitiesRegistry + registry *kcr.CapabilitiesRegistry chain deployment.Chain - nodeIDToParams map[string]capabilities_registry.CapabilitiesRegistryNodeParams - donToCapabilities map[string][]registeredCapability + nodeIDToParams map[string]kcr.CapabilitiesRegistryNodeParams + donToCapabilities map[string][]RegisteredCapability donToOcr2Nodes map[string][]*ocr2Node } type registerDonsResponse struct { - donInfos map[string]capabilities_registry.CapabilitiesRegistryDONInfo + donInfos map[string]kcr.CapabilitiesRegistryDONInfo } func sortedHash(p2pids [][32]byte) string { @@ -708,7 +677,7 @@ func sortedHash(p2pids [][32]byte) string { func registerDons(lggr logger.Logger, req registerDonsRequest) (*registerDonsResponse, error) { resp := registerDonsResponse{ - donInfos: make(map[string]capabilities_registry.CapabilitiesRegistryDONInfo), + donInfos: make(map[string]kcr.CapabilitiesRegistryDONInfo), } // track hash of sorted p2pids to don name because the registry return value does not include the don name // and we need to map it back to the don name to access the other mapping data such as the don's capabilities & nodes @@ -735,7 +704,7 @@ func registerDons(lggr logger.Logger, req registerDonsRequest) (*registerDonsRes return nil, fmt.Errorf("capabilities not found for node operator %s", don) } wfSupported := false - var cfgs []capabilities_registry.CapabilitiesRegistryCapabilityConfiguration + var cfgs []kcr.CapabilitiesRegistryCapabilityConfiguration for _, cap := range caps { if cap.CapabilityType == 2 { // OCR3 capability => WF supported wfSupported = true @@ -746,8 +715,8 @@ func registerDons(lggr logger.Logger, req registerDonsRequest) (*registerDonsRes if err != nil { return nil, fmt.Errorf("failed to marshal capability config for %v: %w", cap, err) } - cfgs = append(cfgs, capabilities_registry.CapabilitiesRegistryCapabilityConfiguration{ - CapabilityId: cap.id, + cfgs = append(cfgs, kcr.CapabilitiesRegistryCapabilityConfiguration{ + CapabilityId: cap.ID, Config: cfgb, }) } diff --git a/integration-tests/deployment/keystone/forwarder_deployer.go b/integration-tests/deployment/keystone/forwarder_deployer.go index 8ec58ebe023..7746cb93593 100644 --- a/integration-tests/deployment/keystone/forwarder_deployer.go +++ b/integration-tests/deployment/keystone/forwarder_deployer.go @@ -18,7 +18,7 @@ var ForwarderTypeVersion = deployment.TypeAndVersion{ Version: deployment.Version1_0_0, } -func (c *KeystoneForwarderDeployer) deploy(req deployRequest) (*deployResponse, error) { +func (c *KeystoneForwarderDeployer) deploy(req DeployRequest) (*DeployResponse, error) { est, err := estimateDeploymentGas(req.Chain.Client, forwarder.KeystoneForwarderABI) if err != nil { return nil, fmt.Errorf("failed to estimate gas: %w", err) @@ -36,7 +36,7 @@ func (c *KeystoneForwarderDeployer) deploy(req deployRequest) (*deployResponse, if err != nil { return nil, fmt.Errorf("failed to confirm and save KeystoneForwarder: %w", err) } - resp := &deployResponse{ + resp := &DeployResponse{ Address: forwarderAddr, Tx: tx.Hash(), Tv: ForwarderTypeVersion, diff --git a/integration-tests/deployment/keystone/ocr3_deployer.go b/integration-tests/deployment/keystone/ocr3_deployer.go index fb1fddfa16c..d840e5250f8 100644 --- a/integration-tests/deployment/keystone/ocr3_deployer.go +++ b/integration-tests/deployment/keystone/ocr3_deployer.go @@ -18,7 +18,7 @@ var OCR3CapabilityTypeVersion = deployment.TypeAndVersion{ Version: deployment.Version1_0_0, } -func (c *OCR3Deployer) deploy(req deployRequest) (*deployResponse, error) { +func (c *OCR3Deployer) deploy(req DeployRequest) (*DeployResponse, error) { est, err := estimateDeploymentGas(req.Chain.Client, ocr3_capability.OCR3CapabilityABI) if err != nil { return nil, fmt.Errorf("failed to estimate gas: %w", err) @@ -36,7 +36,7 @@ func (c *OCR3Deployer) deploy(req deployRequest) (*deployResponse, error) { if err != nil { return nil, fmt.Errorf("failed to confirm transaction %s: %w", tx.Hash().String(), err) } - resp := &deployResponse{ + resp := &DeployResponse{ Address: ocr3Addr, Tx: tx.Hash(), Tv: OCR3CapabilityTypeVersion, diff --git a/integration-tests/deployment/keystone/test/utils.go b/integration-tests/deployment/keystone/test/utils.go new file mode 100644 index 00000000000..911d9612f31 --- /dev/null +++ b/integration-tests/deployment/keystone/test/utils.go @@ -0,0 +1,188 @@ +package test + +import ( + "context" + "fmt" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + "github.com/smartcontractkit/chainlink/integration-tests/deployment/memory" + + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +type SetupTestRegistryRequest struct { + P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc + DonToNodes map[string][]*kslib.P2PSignerEnc +} + +type SetupTestRegistryResponse struct { + Registry *kcr.CapabilitiesRegistry + Chain deployment.Chain +} + +func SetupTestRegistry(t *testing.T, lggr logger.Logger, req *SetupTestRegistryRequest) *SetupTestRegistryResponse { + chain := testChain(t) + // deploy the registry + registry := deployCapReg(t, lggr, chain) + // convert req to nodeoperators + nops := make([]kcr.CapabilitiesRegistryNodeOperator, 0) + for nop := range req.NopToNodes { + nops = append(nops, nop) + } + sort.Slice(nops, func(i, j int) bool { + return nops[i].Name < nops[j].Name + }) + addNopsResp := addNops(t, lggr, chain, registry, nops) + require.Len(t, addNopsResp.Nops, len(nops)) + + // add capabilities to registry + capCache := NewCapabiltyCache(t) + var capabilities []kcr.CapabilitiesRegistryCapability + for _, caps := range req.P2pToCapabilities { + capabilities = append(capabilities, caps...) + } + registeredCapabilities := capCache.AddCapabilities(lggr, chain, registry, capabilities) + expectedDeduped := make(map[kcr.CapabilitiesRegistryCapability]struct{}) + for _, cap := range capabilities { + expectedDeduped[cap] = struct{}{} + } + require.Len(t, registeredCapabilities, len(expectedDeduped)) + + // add the nodes with the phony capabilities. cannot register a node without a capability and capability must exist + // to do this make an initial phony request and extract the node params + initialp2pToCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for p2pID := range req.P2pToCapabilities { + initialp2pToCapabilities[p2pID] = vanillaCapabilities(registeredCapabilities) + } + phonyRequest := &kslib.UpdateNodesRequest{ + Chain: chain, + Registry: registry, + P2pToCapabilities: req.P2pToCapabilities, + NopToNodes: req.NopToNodes, + } + nodeParams, err := phonyRequest.NodeParams() + require.NoError(t, err) + addNodes(t, lggr, chain, registry, nodeParams) + return &SetupTestRegistryResponse{ + Registry: registry, + Chain: chain, + } +} + +func deployCapReg(t *testing.T, lggr logger.Logger, chain deployment.Chain) *kcr.CapabilitiesRegistry { + capabilitiesRegistryDeployer := kslib.NewCapabilitiesRegistryDeployer(lggr) + _, err := capabilitiesRegistryDeployer.Deploy(kslib.DeployRequest{Chain: chain}) + require.NoError(t, err) + return capabilitiesRegistryDeployer.Contract() +} + +func addNops(t *testing.T, lggr logger.Logger, chain deployment.Chain, registry *kcr.CapabilitiesRegistry, nops []kcr.CapabilitiesRegistryNodeOperator) *kslib.RegisterNOPSResponse { + resp, err := kslib.RegisterNOPS(context.TODO(), kslib.RegisterNOPSRequest{ + Chain: chain, + Registry: registry, + Nops: nops, + }) + require.NoError(t, err) + return resp +} + +func addNodes(t *testing.T, lggr logger.Logger, chain deployment.Chain, registry *kcr.CapabilitiesRegistry, nodes []kcr.CapabilitiesRegistryNodeParams) { + tx, err := registry.AddNodes(chain.DeployerKey, nodes) + if err != nil { + err2 := kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + require.Fail(t, fmt.Sprintf("failed to call AddNodes: %s: %s", err, err2)) + } + _, err = chain.Confirm(tx) + require.NoError(t, err) +} + +// CapabilityCache tracks registered capabilities by name +type CapabilityCache struct { + t *testing.T + nameToId map[string][32]byte +} + +func NewCapabiltyCache(t *testing.T) *CapabilityCache { + return &CapabilityCache{ + t: t, + nameToId: make(map[string][32]byte), + } +} + +// AddCapabilities adds the capabilities to the registry and returns the registered capabilities +// if the capability is already registered, it will not be re-registered +// if duplicate capabilities are passed, they will be deduped +func (cc *CapabilityCache) AddCapabilities(lggr logger.Logger, chain deployment.Chain, registry *kcr.CapabilitiesRegistry, capabilities []kcr.CapabilitiesRegistryCapability) []kslib.RegisteredCapability { + t := cc.t + var out []kslib.RegisteredCapability + // get the registered capabilities & dedup + seen := make(map[kcr.CapabilitiesRegistryCapability]struct{}) + var toRegister []kcr.CapabilitiesRegistryCapability + for _, cap := range capabilities { + id, cached := cc.nameToId[kslib.CapabilityID(cap)] + if cached { + out = append(out, kslib.RegisteredCapability{ + CapabilitiesRegistryCapability: cap, + ID: id, + }) + continue + } + // dedup + if _, exists := seen[cap]; !exists { + seen[cap] = struct{}{} + toRegister = append(toRegister, cap) + } + } + if len(toRegister) == 0 { + return out + } + tx, err := registry.AddCapabilities(chain.DeployerKey, toRegister) + if err != nil { + err2 := kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + require.Fail(t, fmt.Sprintf("failed to call AddCapabilities: %s: %s", err, err2)) + } + _, err = chain.Confirm(tx) + require.NoError(t, err) + + // get the registered capabilities + for _, capb := range toRegister { + capb := capb + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, capb.LabelledName, capb.Version) + require.NoError(t, err) + out = append(out, kslib.RegisteredCapability{ + CapabilitiesRegistryCapability: capb, + ID: id, + }) + // cache the id + cc.nameToId[kslib.CapabilityID(capb)] = id + } + return out +} + +func testChain(t *testing.T) deployment.Chain { + chains := memory.NewMemoryChains(t, 1) + var chain deployment.Chain + for _, c := range chains { + chain = c + break + } + require.NotEmpty(t, chain) + return chain +} + +func vanillaCapabilities(rcs []kslib.RegisteredCapability) []kcr.CapabilitiesRegistryCapability { + out := make([]kcr.CapabilitiesRegistryCapability, len(rcs)) + for i := range rcs { + out[i] = rcs[i].CapabilitiesRegistryCapability + } + return out +} diff --git a/integration-tests/deployment/keystone/types.go b/integration-tests/deployment/keystone/types.go index 1613387343d..c9ed421a9d8 100644 --- a/integration-tests/deployment/keystone/types.go +++ b/integration-tests/deployment/keystone/types.go @@ -29,13 +29,13 @@ var ( OCR3Capability deployment.ContractType = "OCR3Capability" ) -type deployResponse struct { +type DeployResponse struct { Address common.Address Tx common.Hash // todo: chain agnostic Tv deployment.TypeAndVersion } -type deployRequest struct { +type DeployRequest struct { Chain deployment.Chain } diff --git a/integration-tests/deployment/keystone/update_nodes.go b/integration-tests/deployment/keystone/update_nodes.go new file mode 100644 index 00000000000..5f15142224c --- /dev/null +++ b/integration-tests/deployment/keystone/update_nodes.go @@ -0,0 +1,202 @@ +package keystone + +import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + + "github.com/smartcontractkit/chainlink/integration-tests/deployment" +) + +type UpdateNodesRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + + P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc +} + +func (req *UpdateNodesRequest) NodeParams() ([]kcr.CapabilitiesRegistryNodeParams, error) { + return makeNodeParams(req.Registry, req.NopToNodes, req.P2pToCapabilities) +} + +// P2PSignerEnc represent the key fields in kcr.CapabilitiesRegistryNodeParams +// these values are obtain-able directly from the offchain node +type P2PSignerEnc struct { + Signer [32]byte + P2PKey p2pkey.PeerID + EncryptionPublicKey [32]byte +} + +func (req *UpdateNodesRequest) Validate() error { + if len(req.P2pToCapabilities) == 0 { + return errors.New("p2pToCapabilities is empty") + } + if len(req.NopToNodes) == 0 { + return errors.New("nopToNodes is empty") + } + if req.Registry == nil { + return errors.New("registry is nil") + } + + return nil +} + +type UpdateNodesResponse struct { + NodeParams []kcr.CapabilitiesRegistryNodeParams +} + +// UpdateNodes updates the nodes in the registry +// the update sets the signer and capabilities for each node. it does not append capabilities to the existing ones +func UpdateNodes(lggr logger.Logger, req *UpdateNodesRequest) (*UpdateNodesResponse, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate request: %w", err) + } + + params, err := req.NodeParams() + if err != nil { + return nil, fmt.Errorf("failed to make node params: %w", err) + } + tx, err := req.Registry.UpdateNodes(req.Chain.DeployerKey, params) + if err != nil { + err = DecodeErr(kcr.CapabilitiesRegistryABI, err) + return nil, fmt.Errorf("failed to call UpdateNodes: %w", err) + } + + _, err = req.Chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm UpdateNodes confirm transaction %s: %w", tx.Hash().String(), err) + } + return &UpdateNodesResponse{NodeParams: params}, nil +} + +// AppendCapabilities appends the capabilities to the existing capabilities of the nodes listed in p2pIds in the registry +func AppendCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, chain deployment.Chain, p2pIds []p2pkey.PeerID, capabilities []kcr.CapabilitiesRegistryCapability) (map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability, error) { + out := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + allCapabilities, err := registry.GetCapabilities(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("failed to GetCapabilities from registry: %w", err) + } + var capMap = make(map[[32]byte]kcr.CapabilitiesRegistryCapability) + for _, cap := range allCapabilities { + capMap[cap.HashedId] = kcr.CapabilitiesRegistryCapability{ + LabelledName: cap.LabelledName, + Version: cap.Version, + CapabilityType: cap.CapabilityType, + ResponseType: cap.ResponseType, + ConfigurationContract: cap.ConfigurationContract, + } + } + + for _, p2pID := range p2pIds { + // read the existing capabilities for the node + info, err := registry.GetNode(&bind.CallOpts{}, p2pID) + if err != nil { + return nil, fmt.Errorf("failed to get node info for %s: %w", p2pID, err) + } + mergedCaps := make([]kcr.CapabilitiesRegistryCapability, 0) + // we only have the id; need to fetch the capabilities details + for _, capID := range info.HashedCapabilityIds { + c, exists := capMap[capID] + if !exists { + return nil, fmt.Errorf("capability not found for %s", capID) + } + mergedCaps = append(mergedCaps, c) + } + // append the new capabilities and dedup + mergedCaps = append(mergedCaps, capabilities...) + var deduped []kcr.CapabilitiesRegistryCapability + seen := make(map[string]struct{}) + for _, cap := range mergedCaps { + if _, ok := seen[CapabilityID(cap)]; !ok { + seen[CapabilityID(cap)] = struct{}{} + deduped = append(deduped, cap) + } + } + out[p2pID] = deduped + } + return out, nil +} + +func makeNodeParams(registry *kcr.CapabilitiesRegistry, + nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc, + p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) ([]kcr.CapabilitiesRegistryNodeParams, error) { + + out := make([]kcr.CapabilitiesRegistryNodeParams, 0) + // get all the node operators from chain + registeredNops, err := registry.GetNodeOperators(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("failed to get node operators: %w", err) + } + + // make a cache of capability from chain + var allCaps []kcr.CapabilitiesRegistryCapability + for _, caps := range p2pToCapabilities { + allCaps = append(allCaps, caps...) + } + capMap, err := fetchCapabilityIDs(registry, allCaps) + if err != nil { + return nil, fmt.Errorf("failed to fetch capability ids: %w", err) + } + + // flatten the onchain state to list of node params filtered by the input nops and nodes + for idx, rnop := range registeredNops { + // nop id is 1-indexed. no way to get value from chain. must infer from index + nopID := uint32(idx + 1) + nodes, ok := nopToNodes[rnop] + if !ok { + continue + } + for _, node := range nodes { + caps, ok := p2pToCapabilities[node.P2PKey] + if !ok { + return nil, fmt.Errorf("capabilities not found for node %s", node.P2PKey) + } + hashedCaps := make([][32]byte, len(caps)) + for i, cap := range caps { + hashedCap, exists := capMap[CapabilityID(cap)] + if !exists { + return nil, fmt.Errorf("capability id not found for %s", CapabilityID(cap)) + } + hashedCaps[i] = hashedCap + } + out = append(out, kcr.CapabilitiesRegistryNodeParams{ + NodeOperatorId: nopID, + P2pId: node.P2PKey, + HashedCapabilityIds: hashedCaps, + EncryptionPublicKey: node.EncryptionPublicKey, + Signer: node.Signer, + }) + } + } + + return out, nil +} + +// CapabilityID returns a unique id for the capability +// TODO: mv to chainlink-common? ref https://github.com/smartcontractkit/chainlink/blob/4fb06b4525f03c169c121a68defa9b13677f5f20/contracts/src/v0.8/keystone/CapabilitiesRegistry.sol#L170 +func CapabilityID(c kcr.CapabilitiesRegistryCapability) string { + return fmt.Sprintf("%s@%s", c.LabelledName, c.Version) +} + +// fetchCapabilityIDs fetches the capability ids for the given capabilities +func fetchCapabilityIDs(registry *kcr.CapabilitiesRegistry, caps []kcr.CapabilitiesRegistryCapability) (map[string][32]byte, error) { + out := make(map[string][32]byte) + for _, cap := range caps { + name := CapabilityID(cap) + if _, exists := out[name]; exists { + continue + } + hashId, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.LabelledName, cap.Version) + if err != nil { + return nil, fmt.Errorf("failed to get capability id for %s: %w", name, err) + } + out[name] = hashId + } + return out, nil +} diff --git a/integration-tests/deployment/keystone/update_nodes_test.go b/integration-tests/deployment/keystone/update_nodes_test.go new file mode 100644 index 00000000000..e5eef02c86a --- /dev/null +++ b/integration-tests/deployment/keystone/update_nodes_test.go @@ -0,0 +1,531 @@ +package keystone_test + +import ( + "bytes" + "fmt" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/integration-tests/deployment" + kslib "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone" + kstest "github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/test" + + "github.com/smartcontractkit/chainlink/integration-tests/deployment/memory" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func Test_UpdateNodesRequest_validate(t *testing.T) { + type fields struct { + p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + //nopToNodes map[uint32][]*kslib.P2PSigner + nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc + chain deployment.Chain + registry *kcr.CapabilitiesRegistry + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "err", + fields: fields{ + p2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{}, + nopToNodes: nil, + chain: deployment.Chain{}, + registry: nil, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := &kslib.UpdateNodesRequest{ + P2pToCapabilities: tt.fields.p2pToCapabilities, + NopToNodes: tt.fields.nopToNodes, + Chain: tt.fields.chain, + Registry: tt.fields.registry, + } + if err := req.Validate(); (err != nil) != tt.wantErr { + t.Errorf("kslib.UpdateNodesRequest.validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestUpdateNodes(t *testing.T) { + chain := testChain(t) + require.NotNil(t, chain) + lggr := logger.Test(t) + + type args struct { + lggr logger.Logger + req *kslib.UpdateNodesRequest + } + tests := []struct { + name string + args args + want *kslib.UpdateNodesResponse + wantErr bool + }{ + { + name: "one node, one capability", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nop1"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + wantErr: false, + }, + { + name: "one node, two capabilities", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 2, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nop1"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + wantErr: false, + }, + { + name: "twos node, one shared capability", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + testPeerID(t, "peerID_2"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nopA"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + testNop(t, "nopB"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + { + NodeOperatorId: 2, + P2pId: testPeerID(t, "peerID_2"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + wantErr: false, + }, + { + name: "twos node, different capabilities", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + testPeerID(t, "peerID_2"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nopA"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + testNop(t, "nopB"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + { + NodeOperatorId: 2, + P2pId: testPeerID(t, "peerID_2"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // need to setup the registry and chain with a phony capability so that there is something to update + var phonyCap = kcr.CapabilitiesRegistryCapability{ + LabelledName: "phony", + Version: "1.0.0", + CapabilityType: 0, + } + initMap := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for p2pID := range tt.args.req.P2pToCapabilities { + initMap[p2pID] = []kcr.CapabilitiesRegistryCapability{phonyCap} + } + setupResp := kstest.SetupTestRegistry(t, tt.args.lggr, &kstest.SetupTestRegistryRequest{ + P2pToCapabilities: initMap, + NopToNodes: tt.args.req.NopToNodes, + }) + registry := setupResp.Registry + tt.args.req.Registry = setupResp.Registry + tt.args.req.Chain = setupResp.Chain + + //registry := kstest.SetupUpdateNodes(t, tt.args.lggr, tt.args.req) + //tt.args.req.Registry = registry + // register the capabilities that the Update will use + expectedUpdatedCaps := make(map[p2pkey.PeerID][]kslib.RegisteredCapability) + capCache := kstest.NewCapabiltyCache(t) + for p2p, newCaps := range tt.args.req.P2pToCapabilities { + expectedCaps := capCache.AddCapabilities(tt.args.lggr, tt.args.req.Chain, registry, newCaps) + expectedUpdatedCaps[p2p] = expectedCaps + } + got, err := kslib.UpdateNodes(tt.args.lggr, tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("UpdateNodes() error = %v, wantErr %v", err, tt.wantErr) + return + } + for i, p := range got.NodeParams { + expected := tt.want.NodeParams[i] + require.Equal(t, expected.NodeOperatorId, p.NodeOperatorId) + require.Equal(t, expected.P2pId, p.P2pId) + require.Equal(t, expected.Signer, p.Signer) + // check the capabilities + expectedCaps := expectedUpdatedCaps[p.P2pId] + var wantHashedIds [][32]byte + for _, cap := range expectedCaps { + wantHashedIds = append(wantHashedIds, cap.ID) + } + sort.Slice(wantHashedIds, func(i, j int) bool { + return bytes.Compare(wantHashedIds[i][:], wantHashedIds[j][:]) < 0 + }) + gotHashedIds := p.HashedCapabilityIds + sort.Slice(gotHashedIds, func(i, j int) bool { + return bytes.Compare(gotHashedIds[i][:], gotHashedIds[j][:]) < 0 + }) + require.Len(t, gotHashedIds, len(wantHashedIds)) + for j, gotCap := range gotHashedIds { + assert.Equal(t, wantHashedIds[j], gotCap) + } + } + }) + } + + // unique cases + t.Run("duplicate update idempotent", func(t *testing.T) { + var ( + p2pToCapabilitiesInitial = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "first", + Version: "1.0.0", + CapabilityType: 0, + }, + { + LabelledName: "second", + Version: "1.0.0", + CapabilityType: 2, + }, + }, + } + p2pToCapabilitiesUpdated = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nopA"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{3: 16, 4: 2}, + }, + }, + } + ) + + // setup registry and add one capability + setupResp := kstest.SetupTestRegistry(t, lggr, &kstest.SetupTestRegistryRequest{ + P2pToCapabilities: p2pToCapabilitiesInitial, + NopToNodes: nopToNodes, + }) + registry := setupResp.Registry + chain := setupResp.Chain + + // there should be two capabilities + info, err := registry.GetNode(&bind.CallOpts{}, testPeerID(t, "peerID_1")) + require.NoError(t, err) + require.Len(t, info.HashedCapabilityIds, 2) + + // update the capabilities, there should be then be one capability + // first update registers the new capability + toRegister := p2pToCapabilitiesUpdated[testPeerID(t, "peerID_1")] + tx, err := registry.AddCapabilities(chain.DeployerKey, toRegister) + if err != nil { + err2 := kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + require.Fail(t, fmt.Sprintf("failed to call AddCapabilities: %s: %s", err, err2)) + } + _, err = chain.Confirm(tx) + require.NoError(t, err) + + var req = &kslib.UpdateNodesRequest{ + P2pToCapabilities: p2pToCapabilitiesUpdated, + NopToNodes: nopToNodes, + Chain: chain, + Registry: registry, + } + _, err = kslib.UpdateNodes(lggr, req) + require.NoError(t, err) + info, err = registry.GetNode(&bind.CallOpts{}, testPeerID(t, "peerID_1")) + require.NoError(t, err) + require.Len(t, info.HashedCapabilityIds, 1) + want := info.HashedCapabilityIds[0] + + // update again and ensure the result is the same + _, err = kslib.UpdateNodes(lggr, req) + require.NoError(t, err) + info, err = registry.GetNode(&bind.CallOpts{}, testPeerID(t, "peerID_1")) + require.NoError(t, err) + require.Len(t, info.HashedCapabilityIds, 1) + got := info.HashedCapabilityIds[0] + assert.Equal(t, want, got) + }) +} + +func TestAppendCapabilities(t *testing.T) { + + var ( + capMap = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSignerEnc{ + testNop(t, "nop"): []*kslib.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + } + ) + lggr := logger.Test(t) + + // setup registry and add one capability + setupResp := kstest.SetupTestRegistry(t, lggr, &kstest.SetupTestRegistryRequest{ + P2pToCapabilities: capMap, + NopToNodes: nopToNodes, + }) + registry := setupResp.Registry + chain := setupResp.Chain + + info, err := registry.GetNode(&bind.CallOpts{}, testPeerID(t, "peerID_1")) + require.NoError(t, err) + require.Len(t, info.HashedCapabilityIds, 1) + // define the new capabilities that should be appended and ensure they are merged with the existing ones + newCaps := []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 0, + }, + { + LabelledName: "cap3", + Version: "1.0.2", + CapabilityType: 0, + }, + } + appendedResp, err := kslib.AppendCapabilities(lggr, registry, chain, []p2pkey.PeerID{testPeerID(t, "peerID_1")}, newCaps) + require.NoError(t, err) + require.Len(t, appendedResp, 1) + gotCaps := appendedResp[testPeerID(t, "peerID_1")] + require.Len(t, gotCaps, 3) + wantCaps := capMap[testPeerID(t, "peerID_1")] + wantCaps = append(wantCaps, newCaps...) + + for i, got := range gotCaps { + assert.Equal(t, kslib.CapabilityID(wantCaps[i]), kslib.CapabilityID(got)) + } + + // trying to append an existing capability should not change the result + appendedResp2, err := kslib.AppendCapabilities(lggr, registry, chain, []p2pkey.PeerID{testPeerID(t, "peerID_1")}, newCaps) + require.NoError(t, err) + require.Len(t, appendedResp2, 1) + gotCaps2 := appendedResp2[testPeerID(t, "peerID_1")] + require.Len(t, gotCaps2, 3) + require.EqualValues(t, gotCaps, gotCaps2) + +} + +func testPeerID(t *testing.T, s string) p2pkey.PeerID { + var out [32]byte + b := []byte(s) + copy(out[:], b) + return p2pkey.PeerID(out) +} + +func testChain(t *testing.T) deployment.Chain { + chains := memory.NewMemoryChains(t, 1) + var chain deployment.Chain + for _, c := range chains { + chain = c + break + } + require.NotEmpty(t, chain) + return chain +} + +func testNop(t *testing.T, name string) kcr.CapabilitiesRegistryNodeOperator { + return kcr.CapabilitiesRegistryNodeOperator{ + Admin: common.HexToAddress("0xFFFFFFFF45297A703e4508186d4C1aa1BAf80000"), + Name: name, + } +}