Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KS-471: append capabilities to nodes in capability registry #14742

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"

Check failure on line 13 in integration-tests/deployment/keystone/changeset/append_node_capabilities_test.go

View workflow job for this annotation

GitHub Actions / Lint integration-tests

could not import github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/changeset (-: # github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/changeset [github.com/smartcontractkit/chainlink/integration-tests/deployment/keystone/changeset.test]
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
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
Loading