diff --git a/deployment/environment/clo/utils.go b/deployment/environment/clo/utils.go new file mode 100644 index 00000000000..79502ef6706 --- /dev/null +++ b/deployment/environment/clo/utils.go @@ -0,0 +1,32 @@ +package clo + +import ( + jd "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" +) + +// NewChainConfig creates a new JobDistributor ChainConfig from a clo model NodeChainConfig +func NewChainConfig(chain *models.NodeChainConfig) *jd.ChainConfig { + return &jd.ChainConfig{ + Chain: &jd.Chain{ + Id: chain.Network.ChainID, + Type: jd.ChainType_CHAIN_TYPE_EVM, // TODO: support other chain types + }, + + AccountAddress: chain.AccountAddress, + AdminAddress: chain.AdminAddress, + Ocr2Config: &jd.OCR2Config{ + Enabled: chain.Ocr2Config.Enabled, + P2PKeyBundle: &jd.OCR2Config_P2PKeyBundle{ + PeerId: chain.Ocr2Config.P2pKeyBundle.PeerID, + PublicKey: chain.Ocr2Config.P2pKeyBundle.PublicKey, + }, + OcrKeyBundle: &jd.OCR2Config_OCRKeyBundle{ + BundleId: chain.Ocr2Config.OcrKeyBundle.BundleID, + OnchainSigningAddress: chain.Ocr2Config.OcrKeyBundle.OnchainSigningAddress, + OffchainPublicKey: chain.Ocr2Config.OcrKeyBundle.OffchainPublicKey, + ConfigPublicKey: chain.Ocr2Config.OcrKeyBundle.ConfigPublicKey, + }, + }, + } +} diff --git a/deployment/keystone/changeset/append_node_capbilities.go b/deployment/keystone/changeset/append_node_capbilities.go index 0cee9b442c8..ae654b7017d 100644 --- a/deployment/keystone/changeset/append_node_capbilities.go +++ b/deployment/keystone/changeset/append_node_capbilities.go @@ -56,6 +56,5 @@ func (req *AppendNodeCapabilitiesRequest) convert(e deployment.Environment) (*in Chain: registryChain, Registry: registry, P2pToCapabilities: req.P2pToCapabilities, - NopToNodes: req.NopToNodes, }, nil } diff --git a/deployment/keystone/changeset/internal/append_node_capabilities.go b/deployment/keystone/changeset/internal/append_node_capabilities.go index f917039e8e0..e74d3178ef2 100644 --- a/deployment/keystone/changeset/internal/append_node_capabilities.go +++ b/deployment/keystone/changeset/internal/append_node_capabilities.go @@ -15,16 +15,12 @@ type AppendNodeCapabilitiesRequest struct { Registry *kcr.CapabilitiesRegistry P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*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") } @@ -59,7 +55,6 @@ func AppendNodeCapabilitiesImpl(lggr logger.Logger, req *AppendNodeCapabilitiesR Chain: req.Chain, Registry: req.Registry, P2pToCapabilities: capsByPeer, - NopToNodes: req.NopToNodes, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { diff --git a/deployment/keystone/changeset/internal/append_node_capabilities_test.go b/deployment/keystone/changeset/internal/append_node_capabilities_test.go index 48a9af3e38d..d28dcd73230 100644 --- a/deployment/keystone/changeset/internal/append_node_capabilities_test.go +++ b/deployment/keystone/changeset/internal/append_node_capabilities_test.go @@ -84,7 +84,6 @@ func TestAppendNodeCapabilities(t *testing.T) { }, }, }, - NopToNodes: nopToNodes, }, }, want: deployment.ChangesetOutput{}, @@ -93,7 +92,6 @@ func TestAppendNodeCapabilities(t *testing.T) { } 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 diff --git a/deployment/keystone/changeset/internal/test/utils.go b/deployment/keystone/changeset/internal/test/utils.go index 64b08a70150..f7ff6845254 100644 --- a/deployment/keystone/changeset/internal/test/utils.go +++ b/deployment/keystone/changeset/internal/test/utils.go @@ -8,21 +8,30 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/environment/memory" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" internal "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" ) +type Don struct { + Name string + P2PIDs []p2pkey.PeerID + CapabilityConfigs []internal.CapabilityConfig +} type SetupTestRegistryRequest struct { P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc - DonToNodes map[string][]*internal.P2PSignerEnc + Dons []Don } type SetupTestRegistryResponse struct { @@ -59,21 +68,32 @@ func SetupTestRegistry(t *testing.T, lggr logger.Logger, req *SetupTestRegistryR } require.Len(t, registeredCapabilities, len(expectedDeduped)) - // add the nodes with the phony capabilities. cannot register a node without a capability and capability must exist - // to do this make an initial phony request and extract the node params - initialp2pToCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + // make the nodes and register node + var nodeParams []kcr.CapabilitiesRegistryNodeParams + initialp2pToCapabilities := make(map[p2pkey.PeerID][][32]byte) for p2pID := range req.P2pToCapabilities { - initialp2pToCapabilities[p2pID] = vanillaCapabilities(registeredCapabilities) + initialp2pToCapabilities[p2pID] = mustCapabilityIds(t, registry, registeredCapabilities) } - phonyRequest := &internal.UpdateNodesRequest{ - Chain: chain, - Registry: registry, - P2pToCapabilities: req.P2pToCapabilities, - NopToNodes: req.NopToNodes, + // create node with initial capabilities assigned to nop + for i, nop := range nops { + if _, exists := req.NopToNodes[nop]; !exists { + require.Fail(t, "missing nopToNodes for %s", nop.Name) + } + for _, p2pSignerEnc := range req.NopToNodes[nop] { + nodeParams = append(nodeParams, kcr.CapabilitiesRegistryNodeParams{ + Signer: p2pSignerEnc.Signer, + P2pId: p2pSignerEnc.P2PKey, + EncryptionPublicKey: p2pSignerEnc.EncryptionPublicKey, + HashedCapabilityIds: initialp2pToCapabilities[p2pSignerEnc.P2PKey], + NodeOperatorId: uint32(i + 1), // nopid in contract is 1-indexed + }) + } } - nodeParams, err := phonyRequest.NodeParams() - require.NoError(t, err) addNodes(t, lggr, chain, registry, nodeParams) + + // add the Dons + addDons(t, lggr, chain, registry, capCache, req.Dons) + return &SetupTestRegistryResponse{ Registry: registry, Chain: chain, @@ -108,6 +128,50 @@ func addNodes(t *testing.T, lggr logger.Logger, chain deployment.Chain, registry require.NoError(t, err) } +func addDons(t *testing.T, lggr logger.Logger, chain deployment.Chain, registry *kcr.CapabilitiesRegistry, capCache *CapabilityCache, dons []Don) { + for _, don := range dons { + acceptsWorkflows := false + // lookup the capabilities + var capConfigs []kcr.CapabilitiesRegistryCapabilityConfiguration + for _, ccfg := range don.CapabilityConfigs { + var cc = kcr.CapabilitiesRegistryCapabilityConfiguration{ + CapabilityId: [32]byte{}, + Config: ccfg.Config, + } + if cc.Config == nil { + cc.Config = defaultCapConfig(t, ccfg.Capability) + } + var exists bool + //var cc kcr.CapabilitiesRegistryCapabilityConfiguration{} + cc.CapabilityId, exists = capCache.Get(ccfg.Capability) + require.True(t, exists, "capability not found in cache %v", ccfg.Capability) + capConfigs = append(capConfigs, cc) + if ccfg.Capability.CapabilityType == 2 { // ocr3 capabilities + acceptsWorkflows = true + } + } + // add the don + isPublic := true + f := len(don.P2PIDs)/3 + 1 + tx, err := registry.AddDON(chain.DeployerKey, internal.PeerIDsToBytes(don.P2PIDs), capConfigs, isPublic, acceptsWorkflows, uint8(f)) + if err != nil { + err2 := kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + require.Fail(t, fmt.Sprintf("failed to call AddDON: %s: %s", err, err2)) + } + _, err = chain.Confirm(tx) + require.NoError(t, err) + } +} + +func defaultCapConfig(t *testing.T, cap kcr.CapabilitiesRegistryCapability) []byte { + empty := &capabilitiespb.CapabilityConfig{ + DefaultConfig: values.Proto(values.EmptyMap()).GetMapValue(), + } + emptyb, err := proto.Marshal(empty) + require.NoError(t, err) + return emptyb +} + // CapabilityCache tracks registered capabilities by name type CapabilityCache struct { t *testing.T @@ -120,6 +184,10 @@ func NewCapabiltyCache(t *testing.T) *CapabilityCache { nameToId: make(map[string][32]byte), } } +func (cc *CapabilityCache) Get(cap kcr.CapabilitiesRegistryCapability) ([32]byte, bool) { + id, exists := cc.nameToId[kslib.CapabilityID(cap)] + return id, exists +} // AddCapabilities adds the capabilities to the registry and returns the registered capabilities // if the capability is already registered, it will not be re-registered @@ -182,10 +250,28 @@ func testChain(t *testing.T) deployment.Chain { return chain } -func vanillaCapabilities(rcs []kslib.RegisteredCapability) []kcr.CapabilitiesRegistryCapability { - out := make([]kcr.CapabilitiesRegistryCapability, len(rcs)) +func capabilityIds(registry *capabilities_registry.CapabilitiesRegistry, rcs []kslib.RegisteredCapability) ([][32]byte, error) { + out := make([][32]byte, len(rcs)) for i := range rcs { - out[i] = rcs[i].CapabilitiesRegistryCapability + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, rcs[i].LabelledName, rcs[i].Version) + if err != nil { + return nil, fmt.Errorf("failed to get capability id: %w", err) + } + out[i] = id } + return out, nil +} + +func mustCapabilityIds(t *testing.T, registry *capabilities_registry.CapabilitiesRegistry, rcs []kslib.RegisteredCapability) [][32]byte { + t.Helper() + out, err := capabilityIds(registry, rcs) + require.NoError(t, err) return out } + +func MustCapabilityId(t *testing.T, registry *capabilities_registry.CapabilitiesRegistry, cap capabilities_registry.CapabilitiesRegistryCapability) [32]byte { + t.Helper() + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.LabelledName, cap.Version) + require.NoError(t, err) + return id +} diff --git a/deployment/keystone/changeset/internal/update_don.go b/deployment/keystone/changeset/internal/update_don.go new file mode 100644 index 00000000000..4883368dc4d --- /dev/null +++ b/deployment/keystone/changeset/internal/update_don.go @@ -0,0 +1,194 @@ +package internal + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "google.golang.org/protobuf/proto" + + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" +) + +// CapabilityConfig is a struct that holds a capability and its configuration +type CapabilityConfig struct { + Capability kcr.CapabilitiesRegistryCapability + Config []byte // this is the marshalled proto config. if nil, a default config is used +} + +type UpdateDonRequest struct { + Registry *kcr.CapabilitiesRegistry + Chain deployment.Chain + + P2PIDs []p2pkey.PeerID // this is the unique identifier for the don + CapabilityConfigs []CapabilityConfig // if Config subfield is nil, a default config is used +} + +func (r *UpdateDonRequest) appendNodeCapabilitiesRequest() *AppendNodeCapabilitiesRequest { + out := &AppendNodeCapabilitiesRequest{ + Chain: r.Chain, + Registry: r.Registry, + P2pToCapabilities: make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability), + } + for _, p2pid := range r.P2PIDs { + if _, exists := out.P2pToCapabilities[p2pid]; !exists { + out.P2pToCapabilities[p2pid] = make([]kcr.CapabilitiesRegistryCapability, 0) + } + for _, cc := range r.CapabilityConfigs { + out.P2pToCapabilities[p2pid] = append(out.P2pToCapabilities[p2pid], cc.Capability) + } + } + return out +} + +func (r *UpdateDonRequest) Validate() error { + if r.Registry == nil { + return fmt.Errorf("registry is required") + } + if len(r.P2PIDs) == 0 { + return fmt.Errorf("p2pIDs is required") + } + return nil +} + +type UpdateDonResponse struct { + DonInfo kcr.CapabilitiesRegistryDONInfo +} + +func UpdateDon(lggr logger.Logger, req *UpdateDonRequest) (*UpdateDonResponse, error) { + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate request: %w", err) + } + + getDonsResp, err := req.Registry.GetDONs(&bind.CallOpts{}) + if err != nil { + return nil, fmt.Errorf("failed to get Dons: %w", err) + } + + don, err := lookupDonByPeerIDs(getDonsResp, req.P2PIDs) + if err != nil { + return nil, fmt.Errorf("failed to lookup don by p2pIDs: %w", err) + } + cfgs, err := computeConfigs(req.Registry, req.CapabilityConfigs, don) + if err != nil { + return nil, fmt.Errorf("failed to compute configs: %w", err) + } + + _, err = AppendNodeCapabilitiesImpl(lggr, req.appendNodeCapabilitiesRequest()) + if err != nil { + return nil, fmt.Errorf("failed to append node capabilities: %w", err) + } + + tx, err := req.Registry.UpdateDON(req.Chain.DeployerKey, don.Id, don.NodeP2PIds, cfgs, don.IsPublic, don.F) + if err != nil { + err = kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + return nil, fmt.Errorf("failed to call UpdateDON: %w", err) + } + + _, err = req.Chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm UpdateDON transaction %s: %w", tx.Hash().String(), err) + } + out := don + out.CapabilityConfigurations = cfgs + return &UpdateDonResponse{DonInfo: out}, nil +} + +func PeerIDsToBytes(p2pIDs []p2pkey.PeerID) [][32]byte { + out := make([][32]byte, len(p2pIDs)) + for i, p2pID := range p2pIDs { + out[i] = p2pID + } + return out +} + +func BytesToPeerIDs(p2pIDs [][32]byte) []p2pkey.PeerID { + out := make([]p2pkey.PeerID, len(p2pIDs)) + for i, p2pID := range p2pIDs { + out[i] = p2pID + } + return out +} + +func computeConfigs(registry *kcr.CapabilitiesRegistry, caps []CapabilityConfig, donInfo kcr.CapabilitiesRegistryDONInfo) ([]kcr.CapabilitiesRegistryCapabilityConfiguration, error) { + out := make([]kcr.CapabilitiesRegistryCapabilityConfiguration, len(caps)) + for i, cap := range caps { + out[i] = kcr.CapabilitiesRegistryCapabilityConfiguration{} + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.Capability.LabelledName, cap.Capability.Version) + if err != nil { + return nil, fmt.Errorf("failed to get capability id: %w", err) + } + out[i].CapabilityId = id + if out[i].Config == nil { + c := kslib.DefaultCapConfig(cap.Capability.CapabilityType, int(donInfo.F)) + cb, err := proto.Marshal(c) + if err != nil { + return nil, fmt.Errorf("failed to marshal capability config for %v: %w", c, err) + } + out[i].Config = cb + } + } + return out, nil +} + +func SortedHash(p2pids [][32]byte) string { + sha256Hash := sha256.New() + sort.Slice(p2pids, func(i, j int) bool { + return bytes.Compare(p2pids[i][:], p2pids[j][:]) < 0 + }) + for _, id := range p2pids { + sha256Hash.Write(id[:]) + } + return hex.EncodeToString(sha256Hash.Sum(nil)) +} + +func lookupDonByPeerIDs(donResp []kcr.CapabilitiesRegistryDONInfo, wanted []p2pkey.PeerID) (kcr.CapabilitiesRegistryDONInfo, error) { + var don kcr.CapabilitiesRegistryDONInfo + wantedDonID := SortedHash(PeerIDsToBytes(wanted)) + found := false + for i, di := range donResp { + gotID := SortedHash(di.NodeP2PIds) + if gotID == wantedDonID { + don = donResp[i] + found = true + break + } + } + if !found { + return don, verboseDonNotFound(donResp, wanted) + } + return don, nil +} + +func verboseDonNotFound(donResp []kcr.CapabilitiesRegistryDONInfo, wanted []p2pkey.PeerID) error { + type debugDonInfo struct { + OnchainID uint32 + P2PIDsHash string + Want []p2pkey.PeerID + Got []p2pkey.PeerID + } + debugIds := make([]debugDonInfo, len(donResp)) + for i, di := range donResp { + debugIds[i] = debugDonInfo{ + OnchainID: di.Id, + P2PIDsHash: SortedHash(di.NodeP2PIds), + Want: wanted, + Got: BytesToPeerIDs(di.NodeP2PIds), + } + } + wantedID := SortedHash(PeerIDsToBytes(wanted)) + b, err2 := json.Marshal(debugIds) + if err2 == nil { + return fmt.Errorf("don not found by p2pIDs %s in %s", wantedID, b) + } + return fmt.Errorf("don not found by p2pIDs %s in %v", wantedID, debugIds) +} diff --git a/deployment/keystone/changeset/internal/update_don_test.go b/deployment/keystone/changeset/internal/update_don_test.go new file mode 100644 index 00000000000..baedda5e93d --- /dev/null +++ b/deployment/keystone/changeset/internal/update_don_test.go @@ -0,0 +1,321 @@ +package internal_test + +import ( + "bytes" + "math/big" + "sort" + "strconv" + "testing" + + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" + kscs "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" + kstest "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal/test" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateDon(t *testing.T) { + var ( + registryChain = chainsel.TEST_90000001 + // nodes + p2p_1 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(100)) + pubKey_1 = "11114981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e1098e7" // valid csa key + admin_1 = common.HexToAddress("0x1111567890123456789012345678901234567890") // valid eth address + signing_1 = "11117293a4cc2621b61193135a95928735e4795f" // valid eth address + node_1 = newNode(t, minimalNodeCfg{ + id: "test node 1", + pubKey: pubKey_1, + registryChain: registryChain, + p2p: p2p_1, + signingAddr: signing_1, + admin: admin_1, + }) + + p2p_2 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(200)) + pubKey_2 = "22224981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109000" // valid csa key + admin_2 = common.HexToAddress("0x2222567890123456789012345678901234567891") // valid eth address + signing_2 = "22227293a4cc2621b61193135a95928735e4ffff" // valid eth address + node_2 = newNode(t, minimalNodeCfg{ + id: "test node 2", + pubKey: pubKey_2, + registryChain: registryChain, + p2p: p2p_2, + signingAddr: signing_2, + admin: admin_2, + }) + + p2p_3 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(300)) + pubKey_3 = "33334981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109111" // valid csa key + admin_3 = common.HexToAddress("0x3333567890123456789012345678901234567892") // valid eth address + signing_3 = "33337293a4cc2621b61193135a959287aaaaffff" // valid eth address + node_3 = newNode(t, minimalNodeCfg{ + id: "test node 3", + pubKey: pubKey_3, + registryChain: registryChain, + p2p: p2p_3, + signingAddr: signing_3, + admin: admin_3, + }) + + p2p_4 = p2pkey.MustNewV2XXXTestingOnly(big.NewInt(400)) + pubKey_4 = "44444981a6119ca3f932cdb8c402d71a72d672adae7849f581ecff8b8e109222" // valid csa key + admin_4 = common.HexToAddress("0x4444567890123456789012345678901234567893") // valid eth address + signing_4 = "44447293a4cc2621b61193135a959287aaaaffff" // valid eth address + node_4 = newNode(t, minimalNodeCfg{ + id: "test node 4", + pubKey: pubKey_4, + registryChain: registryChain, + p2p: p2p_4, + signingAddr: signing_4, + admin: admin_4, + }) + // capabilities + cap_A = kcr.CapabilitiesRegistryCapability{ + LabelledName: "test", + Version: "1.0.0", + CapabilityType: 0, + } + + cap_B = kcr.CapabilitiesRegistryCapability{ + LabelledName: "cap b", + Version: "1.0.0", + CapabilityType: 1, + } + ) + + lggr := logger.Test(t) + + t.Run("empty", func(t *testing.T) { + cfg := setupUpdateDonTestConfig{ + dons: []kslib.DonCapabilities{ + { + Name: "don 1", + Nops: []*models.NodeOperator{ + { + Name: "nop 1", + Nodes: []*models.Node{node_1, node_2, node_3, node_4}, + }, + }, + Capabilities: []kcr.CapabilitiesRegistryCapability{cap_A}, + }, + }, + } + + testCfg := setupUpdateDonTest(t, lggr, cfg) + + req := &internal.UpdateDonRequest{ + Registry: testCfg.Registry, + Chain: testCfg.Chain, + P2PIDs: []p2pkey.PeerID{p2p_1.PeerID(), p2p_2.PeerID(), p2p_3.PeerID(), p2p_4.PeerID()}, + CapabilityConfigs: []internal.CapabilityConfig{ + {Capability: cap_A}, {Capability: cap_B}, + }, + } + want := &internal.UpdateDonResponse{ + DonInfo: kcr.CapabilitiesRegistryDONInfo{ + Id: 1, + ConfigCount: 1, + NodeP2PIds: internal.PeerIDsToBytes([]p2pkey.PeerID{p2p_1.PeerID(), p2p_2.PeerID(), p2p_3.PeerID(), p2p_4.PeerID()}), + CapabilityConfigurations: []kcr.CapabilitiesRegistryCapabilityConfiguration{ + {CapabilityId: kstest.MustCapabilityId(t, testCfg.Registry, cap_A)}, + {CapabilityId: kstest.MustCapabilityId(t, testCfg.Registry, cap_B)}, + }, + }, + } + + got, err := internal.UpdateDon(lggr, req) + require.NoError(t, err) + assert.Equal(t, want.DonInfo.Id, got.DonInfo.Id) + assert.Equal(t, want.DonInfo.ConfigCount, got.DonInfo.ConfigCount) + assert.Equal(t, sortedP2Pids(want.DonInfo.NodeP2PIds), sortedP2Pids(got.DonInfo.NodeP2PIds)) + assert.Equal(t, capIds(want.DonInfo.CapabilityConfigurations), capIds(got.DonInfo.CapabilityConfigurations)) + + }) +} + +func sortedP2Pids(p2pids [][32]byte) [][32]byte { + // sha256Hash := sha256.New() + sort.Slice(p2pids, func(i, j int) bool { + return bytes.Compare(p2pids[i][:], p2pids[j][:]) < 0 + }) + return p2pids +} + +func capIds(ccs []kcr.CapabilitiesRegistryCapabilityConfiguration) [][32]byte { + out := make([][32]byte, len(ccs)) + for i, cc := range ccs { + out[i] = cc.CapabilityId + } + sort.Slice(out, func(i, j int) bool { + return bytes.Compare(out[i][:], out[j][:]) < 0 + }) + return out +} + +type minimalNodeCfg struct { + id string + pubKey string + registryChain chainsel.Chain + p2p p2pkey.KeyV2 + signingAddr string + admin common.Address +} + +func newNode(t *testing.T, cfg minimalNodeCfg) *models.Node { + t.Helper() + + return &models.Node{ + ID: cfg.id, + PublicKey: &cfg.pubKey, + ChainConfigs: []*models.NodeChainConfig{ + { + ID: "test chain", + Network: &models.Network{ + ID: "test network 1", + ChainID: strconv.FormatUint(cfg.registryChain.EvmChainID, 10), + ChainType: models.ChainTypeEvm, + }, + AdminAddress: cfg.admin.String(), + Ocr2Config: &models.NodeOCR2Config{ + P2pKeyBundle: &models.NodeOCR2ConfigP2PKeyBundle{ + PeerID: cfg.p2p.PeerID().String(), + }, + OcrKeyBundle: &models.NodeOCR2ConfigOCRKeyBundle{ + OnchainSigningAddress: cfg.signingAddr, + }, + }, + }, + }, + } +} + +type setupUpdateDonTestConfig struct { + dons []kslib.DonCapabilities +} + +type setupUpdateDonTestResult struct { + registry *kcr.CapabilitiesRegistry + chain deployment.Chain +} + +func setupUpdateDonTest(t *testing.T, lggr logger.Logger, cfg setupUpdateDonTestConfig) *kstest.SetupTestRegistryResponse { + t.Helper() + req := newSetupTestRegistryRequest(t, cfg.dons) + return kstest.SetupTestRegistry(t, lggr, req) +} + +func newSetupTestRegistryRequest(t *testing.T, dons []kslib.DonCapabilities) *kstest.SetupTestRegistryRequest { + t.Helper() + allNops := make(map[string]*models.NodeOperator) + for _, don := range dons { + for _, nop := range don.Nops { + nop := nop + n, exists := allNops[nop.ID] + if exists { + nop.Nodes = append(n.Nodes, nop.Nodes...) + } + allNops[nop.ID] = nop + } + } + var nops []*models.NodeOperator + for _, nop := range allNops { + nops = append(nops, nop) + } + nopsToNodes := makeNopToNodes(t, nops) + testDons := makeTestDon(t, dons) + p2pToCapabilities := makeP2PToCapabilities(t, dons) + req := &kstest.SetupTestRegistryRequest{ + NopToNodes: nopsToNodes, + Dons: testDons, + P2pToCapabilities: p2pToCapabilities, + } + return req +} + +func makeNopToNodes(t *testing.T, cloNops []*models.NodeOperator) map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc { + nopToNodes := make(map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc) + + for _, nop := range cloNops { + // all chain configs are the same wrt admin address & node keys + // so we can just use the first one + crnop := kcr.CapabilitiesRegistryNodeOperator{ + Name: nop.Name, + Admin: common.HexToAddress(nop.Nodes[0].ChainConfigs[0].AdminAddress), + } + var nodes []*internal.P2PSignerEnc + for _, node := range nop.Nodes { + require.NotNil(t, node.PublicKey, "public key is nil %s", node.ID) + // all chain configs are the same wrt admin address & node keys + p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) + require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) + nodes = append(nodes, p) + } + nopToNodes[crnop] = nodes + } + return nopToNodes +} + +func makeP2PToCapabilities(t *testing.T, dons []kslib.DonCapabilities) map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability { + p2pToCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for _, don := range dons { + for _, nop := range don.Nops { + for _, node := range nop.Nodes { + for _, cap := range don.Capabilities { + p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) + require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) + p2pToCapabilities[p.P2PKey] = append(p2pToCapabilities[p.P2PKey], cap) + } + } + } + } + return p2pToCapabilities +} + +func makeTestDon(t *testing.T, dons []kslib.DonCapabilities) []kstest.Don { + out := make([]kstest.Don, len(dons)) + for i, don := range dons { + out[i] = testDon(t, don) + } + return out +} + +func testDon(t *testing.T, don kslib.DonCapabilities) kstest.Don { + var p2pids []p2pkey.PeerID + for _, nop := range don.Nops { + for _, node := range nop.Nodes { + // all chain configs are the same wrt admin address & node keys + // so we can just use the first one + p, err := kscs.NewP2PSignerEncFromCLO(node.ChainConfigs[0], *node.PublicKey) + require.NoError(t, err, "failed to make p2p signer enc from clo nod %s", node.ID) + p2pids = append(p2pids, p.P2PKey) + } + } + + var capabilityConfigs []internal.CapabilityConfig + for _, cap := range don.Capabilities { + capabilityConfigs = append(capabilityConfigs, internal.CapabilityConfig{ + Capability: cap, + }) + } + return kstest.Don{ + Name: don.Name, + P2PIDs: p2pids, + CapabilityConfigs: capabilityConfigs, + } +} + +func newP2PSignerEnc(signer [32]byte, p2pkey p2pkey.PeerID, encryptionPublicKey [32]byte) *internal.P2PSignerEnc { + return &internal.P2PSignerEnc{ + Signer: signer, + P2PKey: p2pkey, + EncryptionPublicKey: encryptionPublicKey, + } +} diff --git a/deployment/keystone/changeset/internal/update_node_capabilities.go b/deployment/keystone/changeset/internal/update_node_capabilities.go index 3d3e1e80607..c7e2e902437 100644 --- a/deployment/keystone/changeset/internal/update_node_capabilities.go +++ b/deployment/keystone/changeset/internal/update_node_capabilities.go @@ -15,16 +15,12 @@ type UpdateNodeCapabilitiesImplRequest struct { Registry *kcr.CapabilitiesRegistry P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc } func (req *UpdateNodeCapabilitiesImplRequest) 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") } @@ -50,7 +46,6 @@ func UpdateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesI Chain: req.Chain, Registry: req.Registry, P2pToCapabilities: req.P2pToCapabilities, - NopToNodes: req.NopToNodes, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { @@ -58,59 +53,3 @@ func UpdateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesI } return resp, nil } - -/* -// 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/deployment/keystone/changeset/internal/update_node_capabilities_test.go b/deployment/keystone/changeset/internal/update_node_capabilities_test.go index d90840f5d13..0346ff20dd6 100644 --- a/deployment/keystone/changeset/internal/update_node_capabilities_test.go +++ b/deployment/keystone/changeset/internal/update_node_capabilities_test.go @@ -8,7 +8,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" - //"github.com/smartcontractkit/chainlink/deployment/keystone/changeset" kslib "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" kstest "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal/test" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" @@ -84,7 +83,6 @@ func TestUpdateNodeCapabilities(t *testing.T) { }, }, }, - NopToNodes: nopToNodes, }, }, want: deployment.ChangesetOutput{}, diff --git a/deployment/keystone/changeset/internal/update_nodes.go b/deployment/keystone/changeset/internal/update_nodes.go index 48face7086b..d263623cdc6 100644 --- a/deployment/keystone/changeset/internal/update_nodes.go +++ b/deployment/keystone/changeset/internal/update_nodes.go @@ -1,8 +1,10 @@ package internal import ( + "bytes" "errors" "fmt" + "sort" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -19,11 +21,10 @@ type UpdateNodesRequest struct { Registry *kcr.CapabilitiesRegistry P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc } func (req *UpdateNodesRequest) NodeParams() ([]kcr.CapabilitiesRegistryNodeParams, error) { - return makeNodeParams(req.Registry, req.NopToNodes, req.P2pToCapabilities) + return makeNodeParams(req.Registry, req.P2pToCapabilities) } // P2PSignerEnc represent the key fields in kcr.CapabilitiesRegistryNodeParams @@ -38,9 +39,18 @@ 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") + // no duplicate capabilities + for peer, caps := range req.P2pToCapabilities { + seen := make(map[string]struct{}) + for _, cap := range caps { + id := kslib.CapabilityID(cap) + if _, exists := seen[id]; exists { + return fmt.Errorf("duplicate capability %s for %s", id, peer) + } + seen[id] = struct{}{} + } } + if req.Registry == nil { return errors.New("registry is nil") } @@ -61,6 +71,7 @@ func UpdateNodes(lggr logger.Logger, req *UpdateNodesRequest) (*UpdateNodesRespo params, err := req.NodeParams() if err != nil { + err = kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) return nil, fmt.Errorf("failed to make node params: %w", err) } tx, err := req.Registry.UpdateNodes(req.Chain.DeployerKey, params) @@ -125,58 +136,45 @@ func AppendCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, } func makeNodeParams(registry *kcr.CapabilitiesRegistry, - nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc, p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) ([]kcr.CapabilitiesRegistryNodeParams, error) { - out := make([]kcr.CapabilitiesRegistryNodeParams, 0) - // get all the node operators from chain - registeredNops, err := registry.GetNodeOperators(&bind.CallOpts{}) - if err != nil { - return nil, fmt.Errorf("failed to get node operators: %w", err) + var out []kcr.CapabilitiesRegistryNodeParams + var p2pIds []p2pkey.PeerID + for p2pID := range p2pToCapabilities { + p2pIds = append(p2pIds, p2pID) } - // make a cache of capability from chain - var allCaps []kcr.CapabilitiesRegistryCapability - for _, caps := range p2pToCapabilities { - allCaps = append(allCaps, caps...) - } - capMap, err := fetchCapabilityIDs(registry, allCaps) + nodes, err := registry.GetNodesByP2PIds(&bind.CallOpts{}, PeerIDsToBytes(p2pIds)) if err != nil { - return nil, fmt.Errorf("failed to fetch capability ids: %w", err) + err = kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) + return nil, fmt.Errorf("failed to get nodes by p2p ids: %w", err) } - - // flatten the onchain state to list of node params filtered by the input nops and nodes - for idx, rnop := range registeredNops { - // nop id is 1-indexed. no way to get value from chain. must infer from index - nopID := uint32(idx + 1) - nodes, ok := nopToNodes[rnop] + for _, node := range nodes { + caps, ok := p2pToCapabilities[node.P2pId] if !ok { - continue + return nil, fmt.Errorf("capabilities not found for node %s", node.P2pId) } - 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[kslib.CapabilityID(cap)] - if !exists { - return nil, fmt.Errorf("capability id not found for %s", kslib.CapabilityID(cap)) - } - hashedCaps[i] = hashedCap - } - out = append(out, kcr.CapabilitiesRegistryNodeParams{ - NodeOperatorId: nopID, - P2pId: node.P2PKey, - HashedCapabilityIds: hashedCaps, - EncryptionPublicKey: node.EncryptionPublicKey, - Signer: node.Signer, - }) + ids, err := capabilityIds(registry, caps) + if err != nil { + return nil, fmt.Errorf("failed to get capability ids: %w", err) } + out = append(out, kcr.CapabilitiesRegistryNodeParams{ + NodeOperatorId: node.NodeOperatorId, + P2pId: node.P2pId, + HashedCapabilityIds: ids, + EncryptionPublicKey: node.EncryptionPublicKey, + Signer: node.Signer, + }) } + sort.Slice(out, func(i, j int) bool { + if out[i].NodeOperatorId == out[j].NodeOperatorId { + return bytes.Compare(out[i].P2pId[:], out[j].P2pId[:]) < 0 + } + return out[i].NodeOperatorId < out[j].NodeOperatorId + }) return out, nil + } // fetchkslib.CapabilityIDs fetches the capability ids for the given capabilities @@ -195,3 +193,15 @@ func fetchCapabilityIDs(registry *kcr.CapabilitiesRegistry, caps []kcr.Capabilit } return out, nil } + +func capabilityIds(registry *kcr.CapabilitiesRegistry, caps []kcr.CapabilitiesRegistryCapability) ([][32]byte, error) { + out := make([][32]byte, len(caps)) + for i, cap := range caps { + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.LabelledName, cap.Version) + if err != nil { + return nil, fmt.Errorf("failed to get capability id: %w", err) + } + out[i] = id + } + return out, nil +} diff --git a/deployment/keystone/changeset/internal/update_nodes_test.go b/deployment/keystone/changeset/internal/update_nodes_test.go index 3515ef13cbe..5488e5c761d 100644 --- a/deployment/keystone/changeset/internal/update_nodes_test.go +++ b/deployment/keystone/changeset/internal/update_nodes_test.go @@ -25,10 +25,9 @@ import ( func Test_UpdateNodesRequest_validate(t *testing.T) { type fields struct { p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - //nopToNodes map[uint32][]*internal.P2PSigner - nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc - chain deployment.Chain - registry *kcr.CapabilitiesRegistry + nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc + chain deployment.Chain + registry *kcr.CapabilitiesRegistry } tests := []struct { name string @@ -50,7 +49,6 @@ func Test_UpdateNodesRequest_validate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { req := &internal.UpdateNodesRequest{ P2pToCapabilities: tt.fields.p2pToCapabilities, - NopToNodes: tt.fields.nopToNodes, Chain: tt.fields.chain, Registry: tt.fields.registry, } @@ -67,8 +65,9 @@ func TestUpdateNodes(t *testing.T) { lggr := logger.Test(t) type args struct { - lggr logger.Logger - req *internal.UpdateNodesRequest + lggr logger.Logger + req *internal.UpdateNodesRequest + nopsToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc } tests := []struct { name string @@ -90,18 +89,18 @@ func TestUpdateNodes(t *testing.T) { }, }, }, - NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ - testNop(t, "nop1"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_1"), - Signer: [32]byte{0: 1, 1: 2}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, - }, - }, Chain: chain, Registry: nil, // set in test to ensure no conflicts }, + nopsToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ + testNop(t, "nop1"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, }, want: &internal.UpdateNodesResponse{ NodeParams: []kcr.CapabilitiesRegistryNodeParams{ @@ -116,6 +115,7 @@ func TestUpdateNodes(t *testing.T) { }, wantErr: false, }, + { name: "one node, two capabilities", args: args{ @@ -135,18 +135,18 @@ func TestUpdateNodes(t *testing.T) { }, }, }, - NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ - testNop(t, "nop1"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_1"), - Signer: [32]byte{0: 1, 1: 2}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, - }, - }, Chain: chain, Registry: nil, // set in test to ensure no conflicts }, + nopsToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ + testNop(t, "nop1"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, + }, + }, + }, }, want: &internal.UpdateNodesResponse{ NodeParams: []kcr.CapabilitiesRegistryNodeParams{ @@ -189,24 +189,24 @@ func TestUpdateNodes(t *testing.T) { }, }, }, - NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ - testNop(t, "nopA"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_1"), - Signer: [32]byte{0: 1, 31: 1}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + nopsToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ + testNop(t, "nopA"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, }, - testNop(t, "nopB"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_2"), - Signer: [32]byte{0: 2, 31: 2}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, + }, + testNop(t, "nopB"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, }, }, - Chain: chain, - Registry: nil, // set in test to ensure no conflicts }, }, want: &internal.UpdateNodesResponse{ @@ -250,24 +250,24 @@ func TestUpdateNodes(t *testing.T) { }, }, }, - NopToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ - testNop(t, "nopA"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_1"), - Signer: [32]byte{0: 1, 31: 1}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, + Chain: chain, + Registry: nil, // set in test to ensure no conflicts + }, + nopsToNodes: map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc{ + testNop(t, "nopA"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 31: 1}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, }, - testNop(t, "nopB"): []*internal.P2PSignerEnc{ - { - P2PKey: testPeerID(t, "peerID_2"), - Signer: [32]byte{0: 2, 31: 2}, - EncryptionPublicKey: [32]byte{0: 7, 1: 7}, - }, + }, + testNop(t, "nopB"): []*internal.P2PSignerEnc{ + { + P2PKey: testPeerID(t, "peerID_2"), + Signer: [32]byte{0: 2, 31: 2}, + EncryptionPublicKey: [32]byte{0: 7, 1: 7}, }, }, - Chain: chain, - Registry: nil, // set in test to ensure no conflicts }, }, want: &internal.UpdateNodesResponse{ @@ -305,14 +305,12 @@ func TestUpdateNodes(t *testing.T) { } setupResp := kstest.SetupTestRegistry(t, tt.args.lggr, &kstest.SetupTestRegistryRequest{ P2pToCapabilities: initMap, - NopToNodes: tt.args.req.NopToNodes, + NopToNodes: tt.args.nopsToNodes, }) 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) @@ -414,7 +412,6 @@ func TestUpdateNodes(t *testing.T) { var req = &internal.UpdateNodesRequest{ P2pToCapabilities: p2pToCapabilitiesUpdated, - NopToNodes: nopToNodes, Chain: chain, Registry: registry, } diff --git a/deployment/keystone/changeset/types.go b/deployment/keystone/changeset/types.go new file mode 100644 index 00000000000..e8a86fa4272 --- /dev/null +++ b/deployment/keystone/changeset/types.go @@ -0,0 +1,50 @@ +package changeset + +import ( + "encoding/hex" + "errors" + "fmt" + + v1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "github.com/smartcontractkit/chainlink/deployment/environment/clo" + "github.com/smartcontractkit/chainlink/deployment/environment/clo/models" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func NewP2PSignerEncFromCLO(cc *models.NodeChainConfig, pubkey string) (*P2PSignerEnc, error) { + ccfg := clo.NewChainConfig(cc) + var pubkeyB [32]byte + if _, err := hex.Decode(pubkeyB[:], []byte(pubkey)); err != nil { + return nil, fmt.Errorf("failed to decode pubkey %s: %w", pubkey, err) + } + return newP2PSignerEncFromJD(ccfg, pubkeyB) +} + +func newP2PSignerEncFromJD(ccfg *v1.ChainConfig, pubkey [32]byte) (*P2PSignerEnc, error) { + if ccfg == nil { + return nil, errors.New("nil ocr2config") + } + ocfg := ccfg.Ocr2Config + p2p := p2pkey.PeerID{} + if err := p2p.UnmarshalString(ocfg.P2PKeyBundle.PeerId); err != nil { + return nil, fmt.Errorf("failed to unmarshal peer id %s: %w", ocfg.P2PKeyBundle.PeerId, err) + } + + signer := ocfg.OcrKeyBundle.OnchainSigningAddress + if len(signer) != 40 { + return nil, fmt.Errorf("invalid onchain signing address %s", ocfg.OcrKeyBundle.OnchainSigningAddress) + } + signerB, err := hex.DecodeString(signer) + if err != nil { + return nil, fmt.Errorf("failed to convert signer %s: %w", signer, err) + } + + var sigb [32]byte + copy(sigb[:], signerB) + + return &P2PSignerEnc{ + Signer: sigb, + P2PKey: p2p, + EncryptionPublicKey: pubkey, // TODO. no current way to get this from the node itself (and therefore not in clo or jd) + }, nil +} diff --git a/deployment/keystone/changeset/update_don.go b/deployment/keystone/changeset/update_don.go new file mode 100644 index 00000000000..1a535c5aa11 --- /dev/null +++ b/deployment/keystone/changeset/update_don.go @@ -0,0 +1,32 @@ +package changeset + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" +) + +var _ deployment.ChangeSet = UpdateDon + +// CapabilityConfig is a struct that holds a capability and its configuration +type CapabilityConfig = internal.CapabilityConfig + +type UpdateDonRequest = internal.UpdateDonRequest + +type UpdateDonResponse struct { + DonInfo kcr.CapabilitiesRegistryDONInfo +} + +// UpdateDon updates the capabilities of a Don +// This a complex action in practice that involves registering missing capabilities, adding the nodes, and updating +// the capabilities of the DON +func UpdateDon(env deployment.Environment, cfg any) (deployment.ChangesetOutput, error) { + req := cfg.(*UpdateDonRequest) + _, err := internal.UpdateDon(env.Logger, req) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to update don: %w", err) + } + return deployment.ChangesetOutput{}, nil +} diff --git a/deployment/keystone/changeset/update_node_capabilities.go b/deployment/keystone/changeset/update_node_capabilities.go index 422411e9061..09cf351cc85 100644 --- a/deployment/keystone/changeset/update_node_capabilities.go +++ b/deployment/keystone/changeset/update_node_capabilities.go @@ -39,7 +39,6 @@ type MutateNodeCapabilitiesRequest struct { RegistryChainSel uint64 P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*P2PSignerEnc } func (req *MutateNodeCapabilitiesRequest) Validate() error { @@ -49,9 +48,6 @@ func (req *MutateNodeCapabilitiesRequest) Validate() error { if len(req.P2pToCapabilities) == 0 { return fmt.Errorf("p2pToCapabilities is empty") } - if len(req.NopToNodes) == 0 { - return fmt.Errorf("nopToNodes is empty") - } _, exists := chainsel.ChainBySelector(req.RegistryChainSel) if !exists { return fmt.Errorf("registry chain selector %d does not exist", req.RegistryChainSel) @@ -84,7 +80,6 @@ func (req *MutateNodeCapabilitiesRequest) updateNodeCapabilitiesImplRequest(e de Chain: registryChain, Registry: registry, P2pToCapabilities: req.P2pToCapabilities, - NopToNodes: req.NopToNodes, }, nil } diff --git a/deployment/keystone/deploy.go b/deployment/keystone/deploy.go index eec648979f4..8838312121a 100644 --- a/deployment/keystone/deploy.go +++ b/deployment/keystone/deploy.go @@ -456,7 +456,7 @@ func RegisterNOPS(ctx context.Context, req RegisterNOPSRequest) (*RegisterNOPSRe return resp, nil } -func defaultCapConfig(capType uint8, nNodes int) *capabilitiespb.CapabilityConfig { +func DefaultCapConfig(capType uint8, nNodes int) *capabilitiespb.CapabilityConfig { switch capType { // TODO: use the enum defined in ?? case uint8(0): // trigger @@ -706,7 +706,7 @@ func registerDons(lggr logger.Logger, req registerDonsRequest) (*registerDonsRes wfSupported = true } // TODO: accept configuration from external source for each (don,capability) - capCfg := defaultCapConfig(cap.CapabilityType, len(p2pIds)) + capCfg := DefaultCapConfig(cap.CapabilityType, len(p2pIds)) cfgb, err := proto.Marshal(capCfg) if err != nil { return nil, fmt.Errorf("failed to marshal capability config for %v: %w", cap, err)