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..2d7a4d8a636 --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/append_node_capabilities_test.go @@ -0,0 +1,146 @@ +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" + "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 TestAppendNodeCapabilities(t *testing.T) { + var ( + initialp2pToCapabilities = map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + p2pId("0x1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "test", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "testNop"): []*kslib.P2PSigner{ + &kslib.P2PSigner{ + Signer: [32]byte{0: 1}, + P2PKey: p2pId("0x1"), + }, + }, + } + ) + + 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{ + p2pId("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 p2pId(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, + } +} 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..32cde1c878f --- /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.P2PSigner +} + +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..71fdf4b4a27 --- /dev/null +++ b/integration-tests/deployment/keystone/changeset/helpers_test.go @@ -0,0 +1,4 @@ +package changeset + +// AppendNodeCapabilitiesImpl a helper exported for testing +var AppendNodeCapabilitiesImpl = appendNodeCapabilitiesImpl 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 1c78986d528..a711eb2ed30 100644 --- a/integration-tests/deployment/keystone/deploy.go +++ b/integration-tests/deployment/keystone/deploy.go @@ -198,15 +198,15 @@ func ConfigureRegistry(ctx context.Context, lggr logger.Logger, req ConfigureCon 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{ @@ -215,7 +215,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) @@ -323,12 +323,12 @@ type registerCapabilitiesRequest struct { } type registerCapabilitiesResponse struct { - donToCapabilities map[string][]registeredCapability + donToCapabilities map[string][]RegisteredCapability } -type registeredCapability struct { +type RegisteredCapability struct { kcr.CapabilitiesRegistryCapability - id [32]byte + ID [32]byte } // registerCapabilities add computes the capability id, adds it to the registry and associates the registered capabilities with appropriate don(s) @@ -337,13 +337,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 { @@ -354,9 +354,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) @@ -369,84 +369,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 *kcr.CapabilitiesRegistry - nops []kcr.CapabilitiesRegistryNodeOperator +type RegisterNOPSRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + Nops []kcr.CapabilitiesRegistryNodeOperator } -type registerNOPSResponse struct { - nops []*kcr.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([]*kcr.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 @@ -516,7 +485,7 @@ type registerNodesRequest struct { chain deployment.Chain nodeIdToNop map[string]kcr.CapabilitiesRegistryNodeOperator donToOcr2Nodes map[string][]*ocr2Node - donToCapabilities map[string][]registeredCapability + donToCapabilities map[string][]RegisteredCapability nops []*kcr.CapabilitiesRegistryNodeOperatorAdded } type registerNodesResponse struct { @@ -557,7 +526,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) @@ -647,7 +616,7 @@ type registerDonsRequest struct { chain deployment.Chain nodeIDToParams map[string]kcr.CapabilitiesRegistryNodeParams - donToCapabilities map[string][]registeredCapability + donToCapabilities map[string][]RegisteredCapability donToOcr2Nodes map[string][]*ocr2Node } @@ -706,7 +675,7 @@ func registerDons(lggr logger.Logger, req registerDonsRequest) (*registerDonsRes return nil, fmt.Errorf("failed to marshal capability config for %v: %w", cap, err) } cfgs = append(cfgs, kcr.CapabilitiesRegistryCapabilityConfiguration{ - CapabilityId: cap.id, + 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..d7668ff95d5 --- /dev/null +++ b/integration-tests/deployment/keystone/test/utils.go @@ -0,0 +1,240 @@ +package test + +import ( + "context" + "fmt" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "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.P2PSigner + DonToNodes map[string][]*kslib.P2PSigner +} + +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 inital 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 SetupUpdateNodes(t *testing.T, lggr logger.Logger, req *kslib.UpdateNodesRequest) *kcr.CapabilitiesRegistry { + // deploy the registry + registry := deployCapReg(t, lggr, req.Chain) + // convert req to nodeoperators + nops := make([]kcr.CapabilitiesRegistryNodeOperator, 0) + for nopID := range req.NopToNodes { + nops = append(nops, kcr.CapabilitiesRegistryNodeOperator{ + Admin: common.HexToAddress("0x900FDC4d45297A743e4508986d4C1aa1BAf89A83"), + Name: fmt.Sprintf("nop_%d", nopID), + }) + } + sort.Slice(nops, func(i, j int) bool { + return nops[i].Name < nops[j].Name + }) + addNopsResp := addNops(t, lggr, req.Chain, registry, nops) + require.Len(t, addNopsResp.Nops, len(nops)) + + // add a fake capability + phonyCap := kcr.CapabilitiesRegistryCapability{ + LabelledName: "phony", + Version: "1.0.0", + CapabilityType: 0, + } + capCache := NewCapabiltyCache(t) + registeredPhonyCaps := capCache.AddCapabilities(lggr, req.Chain, registry, []kcr.CapabilitiesRegistryCapability{phonyCap}) + + // add the nodes with the phony capabilities. cannot register a node without a capability and capability must exist + // to do this make an inital phony request and extract the node params + initialp2pToCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for p2pID := range req.P2pToCapabilities { + initialp2pToCapabilities[p2pID] = vanillaCapabilities(registeredPhonyCaps) + } + phonyRequest := &kslib.UpdateNodesRequest{ + Chain: req.Chain, + Registry: registry, + P2pToCapabilities: initialp2pToCapabilities, + NopToNodes: req.NopToNodes, + } + nodeParams, err := phonyRequest.NodeParams() + require.NoError(t, err) + addNodes(t, lggr, req.Chain, registry, nodeParams) + return registry +} + +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 _, cap := range toRegister { + cap := cap + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.LabelledName, cap.Version) + require.NoError(t, err) + out = append(out, kslib.RegisteredCapability{ + CapabilitiesRegistryCapability: cap, + ID: id, + }) + // cache the id + cc.nameToId[kslib.CapabilityID(cap)] = id + } + return out +} + +func p2pId(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 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 dc0ccea75f8..531c009436c 100644 --- a/integration-tests/deployment/keystone/types.go +++ b/integration-tests/deployment/keystone/types.go @@ -26,13 +26,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 index 4b2d3e497b0..bb6485fd3b3 100644 --- a/integration-tests/deployment/keystone/update_nodes.go +++ b/integration-tests/deployment/keystone/update_nodes.go @@ -1,15 +1,213 @@ package keystone import ( + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" "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" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" ) -type updateNodesRequest struct { - p2pToCapabilities map[string][]registeredCapability - nodes []*ocr2Node +type UpdateNodesRequest struct { + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + + P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSigner +} + +func (req *UpdateNodesRequest) NodeParams() ([]kcr.CapabilitiesRegistryNodeParams, error) { + return makeNodeParams(req.Registry, req.NopToNodes, req.P2pToCapabilities) +} + +type P2PSigner struct { + Signer [32]byte + P2PKey p2pkey.PeerID +} + +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 { + cap, exists := capMap[capID] + if !exists { + return nil, fmt.Errorf("capability not found for %s", capID) + } + mergedCaps = append(mergedCaps, cap) + } + // 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 +} + +type p2pSignerWithNop struct { + P2PSigner + NopID uint32 +} + +func nopEqual(a, b kcr.CapabilitiesRegistryNodeOperator) bool { + return a.Admin.Cmp(b.Admin) == 0 && a.Name == b.Name +} + +// it's not possible to get the nop id from the chain, it is inferred from the order of the nops in the list +func nopId(nop kcr.CapabilitiesRegistryNodeOperator, registeredNops []kcr.CapabilitiesRegistryNodeOperator) (uint32, error) { + for i, r := range registeredNops { + if nopEqual(nop, r) { + return uint32(i), nil + } + } + return 0, fmt.Errorf("nop not found") +} + +func makeNodeParams(registry *kcr.CapabilitiesRegistry, + nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSigner, + 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 + 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, + Signer: node.Signer, + }) + } + } + + return out, nil +} + +func CapabilityID(c kcr.CapabilitiesRegistryCapability) string { + return fmt.Sprintf("%s_%s_%d", c.LabelledName, c.Version, c.CapabilityType) } -func UpdateNodes(lggr logger.Logger, req *registerNodesRequest) (*registerNodesResponse, error) { - req.registry.UpdateNodes(req.chain.DeployerKey, nil) - return nil, nil +// 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..d1e6a38d8b5 --- /dev/null +++ b/integration-tests/deployment/keystone/update_nodes_test.go @@ -0,0 +1,516 @@ +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.P2PSigner + 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{ + p2pId("peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "nop1"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: p2pId("peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + }, + }, + }, + wantErr: false, + }, + { + name: "one node, two capabilities", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + p2pId("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.P2PSigner{ + testNop(t, "nop1"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: p2pId("peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + }, + { + NodeOperatorId: 1, + P2pId: p2pId("peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 1: 2}, + }, + }, + }, + wantErr: false, + }, + { + name: "twos node, one shared capability", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + p2pId("peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + p2pId("peerID_2"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "nopA"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + }, + }, + testNop(t, "nopB"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: p2pId("peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 31: 1}, + }, + { + NodeOperatorId: 2, + P2pId: p2pId("peerID_2"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 2, 31: 2}, + }, + }, + }, + wantErr: false, + }, + { + name: "twos node, different capabilities", + args: args{ + lggr: lggr, + req: &kslib.UpdateNodesRequest{ + P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ + p2pId("peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + p2pId("peerID_2"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 0, + }, + }, + }, + NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "nopA"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + }, + }, + testNop(t, "nopB"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + }, + }, + }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + }, + want: &kslib.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: p2pId("peerID_1"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 1, 31: 1}, + }, + { + NodeOperatorId: 2, + P2pId: p2pId("peerID_2"), + HashedCapabilityIds: nil, // checked dynamically based on the request + Signer: [32]byte{0: 2, 31: 2}, + }, + }, + }, + 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{ + p2pId("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{ + p2pId("peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "nopA"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 1: 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{}, p2pId("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[p2pId("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{}, p2pId("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{}, p2pId("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{ + p2pId("peerID_1"): []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + }, + } + nopToNodes = map[kcr.CapabilitiesRegistryNodeOperator][]*kslib.P2PSigner{ + testNop(t, "nop"): []*kslib.P2PSigner{ + { + P2PKey: p2pId("peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + }, + }, + } + ) + 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{}, p2pId("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{p2pId("peerID_1")}, newCaps) + require.NoError(t, err) + require.Len(t, appendedResp, 1) + gotCaps := appendedResp[p2pId("peerID_1")] + require.Len(t, gotCaps, 3) + wantCaps := capMap[p2pId("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{p2pId("peerID_1")}, newCaps) + require.NoError(t, err) + require.Len(t, appendedResp2, 1) + gotCaps2 := appendedResp2[p2pId("peerID_1")] + require.Len(t, gotCaps2, 3) + require.EqualValues(t, gotCaps, gotCaps2) + +} + +func p2pId(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, + } +}