diff --git a/keystore/go.mod b/keystore/go.mod index f5ccae349..bc549c6a4 100644 --- a/keystore/go.mod +++ b/keystore/go.mod @@ -6,6 +6,7 @@ require ( github.com/ethereum/go-ethereum v1.16.2 github.com/natefinch/atomic v1.0.1 github.com/smartcontractkit/chainlink-common v0.9.6 + github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.42.0 google.golang.org/protobuf v1.36.9 @@ -76,7 +77,6 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect - github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 // indirect github.com/supranational/blst v0.3.14 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/keystore/keystore.go b/keystore/keystore.go index 392fdf7c7..d7dc56319 100644 --- a/keystore/keystore.go +++ b/keystore/keystore.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "slices" + "strings" "sync" "testing" "time" @@ -25,6 +26,38 @@ import ( "github.com/smartcontractkit/chainlink-common/keystore/serialization" ) +type KeyPath []string + +func (k KeyPath) String() string { + return joinKeySegments(k...) +} + +func (k KeyPath) Leaf() string { + return k[len(k)-1] +} + +func NewKeyPath(segments ...string) KeyPath { + return segments +} + +func NewKeyPathFromString(fullName string) KeyPath { + return strings.Split(fullName, "/") +} + +// joinKeySegments joins path-like key name segments using "/" and avoids double slashes. +// Empty segments are skipped so joinKeySegments("EVM", "TX", "my-key") => "EVM/TX/my-key". +func joinKeySegments(segments ...string) string { + cleaned := make([]string, 0, len(segments)) + for _, s := range segments { + s = strings.Trim(s, "/") + if s == "" { + continue + } + cleaned = append(cleaned, s) + } + return strings.Join(cleaned, "/") +} + type KeyType string func (k KeyType) String() string { diff --git a/keystore/keystore_internal_test.go b/keystore/keystore_internal_test.go index c11892589..515a3987a 100644 --- a/keystore/keystore_internal_test.go +++ b/keystore/keystore_internal_test.go @@ -30,3 +30,23 @@ func TestPublicKeyFromPrivateKey(t *testing.T) { // We use SEC1 (uncompressed) format for ECDH public keys. require.Equal(t, 65, len(pubKey)) } + +func TestJoinKeySegments(t *testing.T) { + tests := []struct { + segments []string + expected string + }{ + {segments: []string{"EVM", "TX", "my-key"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "/TX", "my-key"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX/", "my-key"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "/my-key"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "my-key", ""}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "my-key", "/"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "my-key", "//"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "my-key", "///"}, expected: "EVM/TX/my-key"}, + {segments: []string{"EVM", "TX", "my-key", "////"}, expected: "EVM/TX/my-key"}, + } + for _, tt := range tests { + require.Equal(t, tt.expected, joinKeySegments(tt.segments...)) + } +} diff --git a/keystore/ragep2p/peer.go b/keystore/ragep2p/peer.go new file mode 100644 index 000000000..f954476ee --- /dev/null +++ b/keystore/ragep2p/peer.go @@ -0,0 +1,99 @@ +package ragep2p + +import ( + "context" + "crypto/ed25519" + "errors" + + commonks "github.com/smartcontractkit/chainlink-common/keystore" + ragetypes "github.com/smartcontractkit/libocr/ragep2p/types" +) + +var _ ragetypes.PeerKeyring = (*PeerKeyring)(nil) + +const ( + PeerKeyringPrefix = "ragep2p_peer" +) + +type PeerKeyring struct { + ks commonks.Keystore + keyPath commonks.KeyPath + pubKey ragetypes.PeerPublicKey +} + +func (k *PeerKeyring) KeyPath() commonks.KeyPath { + return k.keyPath +} + +func (k *PeerKeyring) PublicKey() ragetypes.PeerPublicKey { + return k.pubKey +} + +func (k *PeerKeyring) PeerID() (string, error) { + peerID, err := ragetypes.PeerIDFromPublicKey(ed25519.PublicKey(k.pubKey[:])) + if err != nil { + return "", err + } + return peerID.String(), nil +} + +func (k *PeerKeyring) MustPeerID() string { + peerID, err := k.PeerID() + if err != nil { + panic(err) + } + return peerID +} + +func (k *PeerKeyring) Sign(msg []byte) ([]byte, error) { + resp, err := k.ks.Sign(context.Background(), commonks.SignRequest{ + KeyName: k.keyPath.String(), + Data: msg, + }) + if err != nil { + return nil, err + } + return resp.Signature, nil +} + +func CreatePeerKeyring(ctx context.Context, ks commonks.Keystore, name string) (*PeerKeyring, error) { + keyPath := commonks.NewKeyPath(PeerKeyringPrefix, name) + createReq := commonks.CreateKeysRequest{ + Keys: []commonks.CreateKeyRequest{ + {KeyName: keyPath.String(), KeyType: commonks.Ed25519}, + }, + } + resp, err := ks.CreateKeys(ctx, createReq) + if err != nil { + return nil, err + } + if len(resp.Keys) != 1 { + return nil, errors.New("expected 1 key") + } + var peerPubKey ragetypes.PeerPublicKey + copy(peerPubKey[:], resp.Keys[0].KeyInfo.PublicKey) + return &PeerKeyring{ks: ks, keyPath: keyPath, pubKey: peerPubKey}, nil +} + +func GetPeerKeyrings(ctx context.Context, ks commonks.Keystore, keyRingNames []string) ([]*PeerKeyring, error) { + var keyNames []string + if len(keyRingNames) > 0 { + for _, name := range keyRingNames { + keyNames = append(keyNames, commonks.NewKeyPath(PeerKeyringPrefix, name).String()) + } + } + keys, err := ks.GetKeys(ctx, commonks.GetKeysRequest{ + KeyNames: keyNames, + }) + if err != nil { + return nil, errors.Join(errors.New("failed to list peer keyrings"), err) + } + var peerKeyrings []*PeerKeyring + for _, key := range keys.Keys { + keyPath := commonks.NewKeyPathFromString(key.KeyInfo.Name) + var peerPubKey ragetypes.PeerPublicKey + copy(peerPubKey[:], key.KeyInfo.PublicKey) + peerKeyrings = append(peerKeyrings, &PeerKeyring{ks: ks, keyPath: keyPath, pubKey: peerPubKey}) + } + return peerKeyrings, nil +} diff --git a/keystore/ragep2p/peer_test.go b/keystore/ragep2p/peer_test.go new file mode 100644 index 000000000..0a0e148c1 --- /dev/null +++ b/keystore/ragep2p/peer_test.go @@ -0,0 +1,57 @@ +package ragep2p_test + +import ( + "testing" + + commonks "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/ragep2p" + "github.com/stretchr/testify/require" +) + +func TestPeerKeyring(t *testing.T) { + storage := commonks.NewMemoryStorage() + ctx := t.Context() + ks, err := commonks.LoadKeystore(ctx, storage, "test-password") + require.NoError(t, err) + peerKeyring, err := ragep2p.CreatePeerKeyring(ctx, ks, "test-peer-keyring") + require.NoError(t, err) + msg := []byte("test-message") + signature, err := peerKeyring.Sign(msg) + require.NoError(t, err) + require.NotNil(t, signature) + + peerKeyrings, err := ragep2p.GetPeerKeyrings(ctx, ks, []string{"test-peer-keyring"}) + require.NoError(t, err) + require.Equal(t, 1, len(peerKeyrings)) + require.Equal(t, peerKeyring.PublicKey(), peerKeyrings[0].PublicKey()) + require.Equal(t, peerKeyring.KeyPath(), peerKeyrings[0].KeyPath()) + + // List all works + peerKeyRings, err := ragep2p.GetPeerKeyrings(ctx, ks, []string{}) + require.NoError(t, err) + require.Equal(t, 1, len(peerKeyRings)) + + // List non-existent errors. + peerKeyRings, err = ragep2p.GetPeerKeyrings(ctx, ks, []string{"non-existent-peer-keyring"}) + require.Error(t, err) + + // Can create multiple. + peerKeyring2, err := ragep2p.CreatePeerKeyring(ctx, ks, "test-peer-keyring-2") + require.NoError(t, err) + msg2 := []byte("test-message-2") + signature2, err := peerKeyring2.Sign(msg2) + require.NoError(t, err) + require.NotNil(t, signature2) + + // List by name works. + peerKeyRings, err = ragep2p.GetPeerKeyrings(ctx, ks, []string{"test-peer-keyring-2"}) + require.NoError(t, err) + require.Equal(t, 1, len(peerKeyRings)) + require.Equal(t, peerKeyring2.PublicKey(), peerKeyRings[0].PublicKey()) + require.Equal(t, peerKeyring2.KeyPath(), peerKeyRings[0].KeyPath()) + + // List all works with multiple. + peerKeyRings, err = ragep2p.GetPeerKeyrings(ctx, ks, []string{}) + require.NoError(t, err) + require.Equal(t, 2, len(peerKeyRings)) +}