Skip to content

Commit

Permalink
KS-471: update capabilities of nodes in capability registry (#14742)
Browse files Browse the repository at this point in the history
* cleanup name and better error decoding

* update deployment owners

* wip

* support adding capabilities existing nodes

* linter

* add UpdateNodeCapability changeset func; fix bad merge; minor renames

* add missing files

* linter
  • Loading branch information
krehermann authored Oct 29, 2024
1 parent ccd9956 commit 5b66644
Show file tree
Hide file tree
Showing 15 changed files with 1,470 additions and 98 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,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,
}
}
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.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
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 5b66644

Please sign in to comment.