Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion keystore/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions keystore/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"slices"
"strings"
"sync"
"testing"
"time"
Expand All @@ -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 {
Expand Down Expand Up @@ -310,3 +343,17 @@ func (k *keystore) save(ctx context.Context, keystore map[string]key) error {
}
return nil
}

// 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, "/")
}
20 changes: 20 additions & 0 deletions keystore/keystore_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...))
}
}
99 changes: 99 additions & 0 deletions keystore/ragep2p/peer.go
Original file line number Diff line number Diff line change
@@ -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
}
57 changes: 57 additions & 0 deletions keystore/ragep2p/peer_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Loading