diff --git a/deployment/keystone/changeset/internal/append_node_capabilities.go b/deployment/keystone/changeset/internal/append_node_capabilities.go index e74d3178ef2..cb28c03c6f5 100644 --- a/deployment/keystone/changeset/internal/append_node_capabilities.go +++ b/deployment/keystone/changeset/internal/append_node_capabilities.go @@ -42,19 +42,19 @@ func AppendNodeCapabilitiesImpl(lggr logger.Logger, req *AppendNodeCapabilitiesR } // for each node, merge the new capabilities with the existing ones and update the node - capsByPeer := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + updatesByPeer := make(map[p2pkey.PeerID]NodeUpdate) for p2pID, caps := range req.P2pToCapabilities { caps, err := 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] + updatesByPeer[p2pID] = NodeUpdate{Capabilities: caps[p2pID]} } updateNodesReq := &UpdateNodesRequest{ - Chain: req.Chain, - Registry: req.Registry, - P2pToCapabilities: capsByPeer, + Chain: req.Chain, + Registry: req.Registry, + P2pToUpdates: updatesByPeer, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { diff --git a/deployment/keystone/changeset/internal/update_node_capabilities.go b/deployment/keystone/changeset/internal/update_node_capabilities.go index c7e2e902437..0420c46f27d 100644 --- a/deployment/keystone/changeset/internal/update_node_capabilities.go +++ b/deployment/keystone/changeset/internal/update_node_capabilities.go @@ -42,10 +42,15 @@ func UpdateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesI return nil, fmt.Errorf("failed to add capabilities: %w", err) } + p2pToUpdates := map[p2pkey.PeerID]NodeUpdate{} + for id, caps := range req.P2pToCapabilities { + p2pToUpdates[id] = NodeUpdate{Capabilities: caps} + } + updateNodesReq := &UpdateNodesRequest{ - Chain: req.Chain, - Registry: req.Registry, - P2pToCapabilities: req.P2pToCapabilities, + Chain: req.Chain, + Registry: req.Registry, + P2pToUpdates: p2pToUpdates, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { diff --git a/deployment/keystone/changeset/internal/update_nodes.go b/deployment/keystone/changeset/internal/update_nodes.go index d263623cdc6..b8a08c37e50 100644 --- a/deployment/keystone/changeset/internal/update_nodes.go +++ b/deployment/keystone/changeset/internal/update_nodes.go @@ -2,6 +2,7 @@ package internal import ( "bytes" + "encoding/hex" "errors" "fmt" "sort" @@ -16,15 +17,23 @@ import ( kslib "github.com/smartcontractkit/chainlink/deployment/keystone" ) +type NodeUpdate struct { + EncryptionPublicKey string + NodeOperatorID uint32 + Signer [32]byte + + Capabilities []kcr.CapabilitiesRegistryCapability +} + type UpdateNodesRequest struct { Chain deployment.Chain Registry *kcr.CapabilitiesRegistry - P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + P2pToUpdates map[p2pkey.PeerID]NodeUpdate } func (req *UpdateNodesRequest) NodeParams() ([]kcr.CapabilitiesRegistryNodeParams, error) { - return makeNodeParams(req.Registry, req.P2pToCapabilities) + return makeNodeParams(req.Registry, req.P2pToUpdates) } // P2PSignerEnc represent the key fields in kcr.CapabilitiesRegistryNodeParams @@ -36,19 +45,30 @@ type P2PSignerEnc struct { } func (req *UpdateNodesRequest) Validate() error { - if len(req.P2pToCapabilities) == 0 { + if len(req.P2pToUpdates) == 0 { return errors.New("p2pToCapabilities is empty") } // no duplicate capabilities - for peer, caps := range req.P2pToCapabilities { + for peer, updates := range req.P2pToUpdates { seen := make(map[string]struct{}) - for _, cap := range caps { + for _, cap := range updates.Capabilities { id := kslib.CapabilityID(cap) if _, exists := seen[id]; exists { return fmt.Errorf("duplicate capability %s for %s", id, peer) } seen[id] = struct{}{} } + + if updates.EncryptionPublicKey != "" { + pk, err := hex.DecodeString(updates.EncryptionPublicKey) + if err != nil { + return fmt.Errorf("invalid public key: could not hex decode: %w", err) + } + + if len(pk) != 32 { + return fmt.Errorf("invalid public key: got len %d, need 32", len(pk)) + } + } } if req.Registry == nil { @@ -136,11 +156,11 @@ func AppendCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, } func makeNodeParams(registry *kcr.CapabilitiesRegistry, - p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) ([]kcr.CapabilitiesRegistryNodeParams, error) { + p2pToUpdates map[p2pkey.PeerID]NodeUpdate) ([]kcr.CapabilitiesRegistryNodeParams, error) { var out []kcr.CapabilitiesRegistryNodeParams var p2pIds []p2pkey.PeerID - for p2pID := range p2pToCapabilities { + for p2pID := range p2pToUpdates { p2pIds = append(p2pIds, p2pID) } @@ -150,20 +170,46 @@ func makeNodeParams(registry *kcr.CapabilitiesRegistry, return nil, fmt.Errorf("failed to get nodes by p2p ids: %w", err) } for _, node := range nodes { - caps, ok := p2pToCapabilities[node.P2pId] + updates, ok := p2pToUpdates[node.P2pId] if !ok { return nil, fmt.Errorf("capabilities not found for node %s", node.P2pId) } - ids, err := capabilityIds(registry, caps) - if err != nil { - return nil, fmt.Errorf("failed to get capability ids: %w", err) + + ids := node.HashedCapabilityIds + if len(updates.Capabilities) > 0 { + is, err := capabilityIds(registry, updates.Capabilities) + if err != nil { + return nil, fmt.Errorf("failed to get capability ids: %w", err) + } + ids = is + } + + encryptionKey := node.EncryptionPublicKey + if updates.EncryptionPublicKey != "" { + pk, err := hex.DecodeString(updates.EncryptionPublicKey) + if err != nil { + return nil, fmt.Errorf("failed to decode encryption public key: %w", err) + } + encryptionKey = [32]byte(pk) } + + signer := node.Signer + var zero [32]byte + if !bytes.Equal(updates.Signer[:], zero[:]) { + signer = updates.Signer + } + + nodeOperatorID := node.NodeOperatorId + if updates.NodeOperatorID != 0 { + nodeOperatorID = updates.NodeOperatorID + } + out = append(out, kcr.CapabilitiesRegistryNodeParams{ - NodeOperatorId: node.NodeOperatorId, + NodeOperatorId: nodeOperatorID, P2pId: node.P2pId, HashedCapabilityIds: ids, - EncryptionPublicKey: node.EncryptionPublicKey, - Signer: node.Signer, + EncryptionPublicKey: encryptionKey, + Signer: signer, }) } sort.Slice(out, func(i, j int) bool { diff --git a/deployment/keystone/changeset/internal/update_nodes_test.go b/deployment/keystone/changeset/internal/update_nodes_test.go index 5488e5c761d..395f1060465 100644 --- a/deployment/keystone/changeset/internal/update_nodes_test.go +++ b/deployment/keystone/changeset/internal/update_nodes_test.go @@ -2,6 +2,8 @@ package internal_test import ( "bytes" + "crypto/rand" + "encoding/hex" "fmt" "sort" "testing" @@ -24,10 +26,10 @@ import ( func Test_UpdateNodesRequest_validate(t *testing.T) { type fields struct { - p2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability - nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc - chain deployment.Chain - registry *kcr.CapabilitiesRegistry + p2pToUpdates map[p2pkey.PeerID]internal.NodeUpdate + nopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc + chain deployment.Chain + registry *kcr.CapabilitiesRegistry } tests := []struct { name string @@ -37,10 +39,38 @@ func Test_UpdateNodesRequest_validate(t *testing.T) { { name: "err", fields: fields{ - p2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{}, - nopToNodes: nil, - chain: deployment.Chain{}, - registry: nil, + p2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{}, + nopToNodes: nil, + chain: deployment.Chain{}, + registry: nil, + }, + wantErr: true, + }, + { + name: "invalid encryption key -- cannot decode", + fields: fields{ + p2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + p2pkey.PeerID{}: { + EncryptionPublicKey: "jk", + }, + }, + nopToNodes: nil, + chain: deployment.Chain{}, + registry: nil, + }, + wantErr: true, + }, + { + name: "invalid encryption key -- invalid length", + fields: fields{ + p2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): { + EncryptionPublicKey: "aabb", + }, + }, + nopToNodes: nil, + chain: deployment.Chain{}, + registry: nil, }, wantErr: true, }, @@ -48,9 +78,9 @@ func Test_UpdateNodesRequest_validate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := &internal.UpdateNodesRequest{ - P2pToCapabilities: tt.fields.p2pToCapabilities, - Chain: tt.fields.chain, - Registry: tt.fields.registry, + P2pToUpdates: tt.fields.p2pToUpdates, + Chain: tt.fields.chain, + Registry: tt.fields.registry, } if err := req.Validate(); (err != nil) != tt.wantErr { t.Errorf("internal.UpdateNodesRequest.validate() error = %v, wantErr %v", err, tt.wantErr) @@ -59,10 +89,18 @@ func Test_UpdateNodesRequest_validate(t *testing.T) { } } +func newEncryptionKey() [32]byte { + key := make([]byte, 32) + rand.Read(key) + return [32]byte(key) +} + func TestUpdateNodes(t *testing.T) { chain := testChain(t) require.NotNil(t, chain) lggr := logger.Test(t) + newKey := newEncryptionKey() + newKeyStr := hex.EncodeToString(newKey[:]) type args struct { lggr logger.Logger @@ -80,12 +118,14 @@ func TestUpdateNodes(t *testing.T) { args: args{ lggr: lggr, req: &internal.UpdateNodesRequest{ - P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ - testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap1", - Version: "1.0.0", - CapabilityType: 0, + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): { + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, }, }, }, @@ -115,23 +155,24 @@ func TestUpdateNodes(t *testing.T) { }, wantErr: false, }, - { name: "one node, two capabilities", args: args{ lggr: lggr, req: &internal.UpdateNodesRequest{ - P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ - testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap1", - Version: "1.0.0", - CapabilityType: 0, - }, - { - LabelledName: "cap2", - Version: "1.0.1", - CapabilityType: 2, + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): internal.NodeUpdate{ + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 2, + }, }, }, }, @@ -173,19 +214,23 @@ func TestUpdateNodes(t *testing.T) { args: args{ lggr: lggr, req: &internal.UpdateNodesRequest{ - P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ - testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap1", - Version: "1.0.0", - CapabilityType: 0, + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): internal.NodeUpdate{ + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, }, }, - testPeerID(t, "peerID_2"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap1", - Version: "1.0.0", - CapabilityType: 0, + testPeerID(t, "peerID_2"): internal.NodeUpdate{ + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, }, }, }, @@ -234,19 +279,23 @@ func TestUpdateNodes(t *testing.T) { args: args{ lggr: lggr, req: &internal.UpdateNodesRequest{ - P2pToCapabilities: map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability{ - testPeerID(t, "peerID_1"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap1", - Version: "1.0.0", - CapabilityType: 0, + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): internal.NodeUpdate{ + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap1", + Version: "1.0.0", + CapabilityType: 0, + }, }, }, - testPeerID(t, "peerID_2"): []kcr.CapabilitiesRegistryCapability{ - { - LabelledName: "cap2", - Version: "1.0.1", - CapabilityType: 0, + testPeerID(t, "peerID_2"): internal.NodeUpdate{ + Capabilities: []kcr.CapabilitiesRegistryCapability{ + { + LabelledName: "cap2", + Version: "1.0.1", + CapabilityType: 0, + }, }, }, }, @@ -290,6 +339,111 @@ func TestUpdateNodes(t *testing.T) { }, wantErr: false, }, + { + name: "one node, updated encryption key", + args: args{ + lggr: lggr, + req: &internal.UpdateNodesRequest{ + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): { + EncryptionPublicKey: newKeyStr, + }, + }, + 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: 1, 1: 2}, + }, + }, + }, + }, + want: &internal.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: newKey, + }, + }, + }, + wantErr: false, + }, + { + name: "one node, updated signer", + args: args{ + lggr: lggr, + req: &internal.UpdateNodesRequest{ + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): { + Signer: [32]byte{0: 2, 1: 3}, + }, + }, + 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: 1, 1: 2}, + }, + }, + }, + }, + want: &internal.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 1, + P2pId: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 2, 1: 3}, + EncryptionPublicKey: [32]byte{0: 1, 1: 2}, + }, + }, + }, + wantErr: false, + }, + { + name: "one node, updated nodeOperatorID", + args: args{ + lggr: lggr, + req: &internal.UpdateNodesRequest{ + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): { + NodeOperatorID: 2, + }, + }, + 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: 1, 1: 2}, + }, + }, + }, + }, + want: &internal.UpdateNodesResponse{ + NodeParams: []kcr.CapabilitiesRegistryNodeParams{ + { + NodeOperatorId: 2, + P2pId: testPeerID(t, "peerID_1"), + Signer: [32]byte{0: 1, 1: 2}, + EncryptionPublicKey: [32]byte{0: 1, 1: 2}, + }, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -300,7 +454,7 @@ func TestUpdateNodes(t *testing.T) { CapabilityType: 0, } initMap := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) - for p2pID := range tt.args.req.P2pToCapabilities { + for p2pID := range tt.args.req.P2pToUpdates { initMap[p2pID] = []kcr.CapabilitiesRegistryCapability{phonyCap} } setupResp := kstest.SetupTestRegistry(t, tt.args.lggr, &kstest.SetupTestRegistryRequest{ @@ -311,12 +465,21 @@ func TestUpdateNodes(t *testing.T) { tt.args.req.Registry = setupResp.Registry tt.args.req.Chain = setupResp.Chain + id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, phonyCap.LabelledName, phonyCap.Version) + require.NoError(t, err) + // 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 + for p2p, update := range tt.args.req.P2pToUpdates { + if len(update.Capabilities) > 0 { + expectedCaps := capCache.AddCapabilities(tt.args.lggr, tt.args.req.Chain, registry, update.Capabilities) + expectedUpdatedCaps[p2p] = expectedCaps + } else { + expectedUpdatedCaps[p2p] = []kslib.RegisteredCapability{ + {CapabilitiesRegistryCapability: phonyCap, ID: id}, + } + } } got, err := internal.UpdateNodes(tt.args.lggr, tt.args.req) if (err != nil) != tt.wantErr { @@ -328,6 +491,7 @@ func TestUpdateNodes(t *testing.T) { require.Equal(t, expected.NodeOperatorId, p.NodeOperatorId) require.Equal(t, expected.P2pId, p.P2pId) require.Equal(t, expected.Signer, p.Signer) + require.Equal(t, expected.EncryptionPublicKey, p.EncryptionPublicKey) // check the capabilities expectedCaps := expectedUpdatedCaps[p.P2pId] var wantHashedIds [][32]byte @@ -411,9 +575,13 @@ func TestUpdateNodes(t *testing.T) { require.NoError(t, err) var req = &internal.UpdateNodesRequest{ - P2pToCapabilities: p2pToCapabilitiesUpdated, - Chain: chain, - Registry: registry, + P2pToUpdates: map[p2pkey.PeerID]internal.NodeUpdate{ + testPeerID(t, "peerID_1"): internal.NodeUpdate{ + Capabilities: toRegister, + }, + }, + Chain: chain, + Registry: registry, } _, err = internal.UpdateNodes(lggr, req) require.NoError(t, err) diff --git a/deployment/keystone/changeset/update_nodes.go b/deployment/keystone/changeset/update_nodes.go new file mode 100644 index 00000000000..7e436160d2e --- /dev/null +++ b/deployment/keystone/changeset/update_nodes.go @@ -0,0 +1,24 @@ +package changeset + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" +) + +var _ deployment.ChangeSet[*UpdateNodesRequest] = UpdateNodes + +type UpdateNodesRequest = internal.UpdateNodesRequest +type NodeUpdate = internal.NodeUpdate + +// UpdateNodes updates the a set of nodes. +// This a complex action in practice that involves registering missing capabilities, adding the nodes, and updating +// the capabilities of the DON +func UpdateNodes(env deployment.Environment, req *UpdateNodesRequest) (deployment.ChangesetOutput, error) { + _, err := internal.UpdateNodes(env.Logger, req) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to update don: %w", err) + } + return deployment.ChangesetOutput{}, nil +}