Skip to content
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
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
33 changes: 33 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
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