Skip to content

Commit

Permalink
support adding capabilities existing nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
krehermann committed Oct 11, 2024
1 parent 574fd93 commit 0ffcb9c
Show file tree
Hide file tree
Showing 13 changed files with 1,307 additions and 82 deletions.
64 changes: 64 additions & 0 deletions integration-tests/deployment/keystone/capability_management.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package changeset

// AppendNodeCapabilitiesImpl a helper exported for testing
var AppendNodeCapabilitiesImpl = appendNodeCapabilitiesImpl
6 changes: 3 additions & 3 deletions integration-tests/deployment/keystone/contract_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit 0ffcb9c

Please sign in to comment.