Skip to content

Commit ec6d837

Browse files
authored
[ARCH-334] Pre-requisites for EVM support (#1664)
* Wip * Switch to get for more consistent naming * mod tidy * Fix test * Remove dup fn * Offchain keyring also family agnostic * Rename
1 parent a71a2d5 commit ec6d837

File tree

7 files changed

+437
-1
lines changed

7 files changed

+437
-1
lines changed

keystore/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/ethereum/go-ethereum v1.16.2
77
github.com/natefinch/atomic v1.0.1
88
github.com/smartcontractkit/chainlink-common v0.9.6
9+
github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358
910
github.com/stretchr/testify v1.10.0
1011
golang.org/x/crypto v0.42.0
1112
google.golang.org/protobuf v1.36.9
@@ -76,7 +77,6 @@ require (
7677
github.com/shopspring/decimal v1.4.0 // indirect
7778
github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.4 // indirect
7879
github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect
79-
github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 // indirect
8080
github.com/supranational/blst v0.3.14 // indirect
8181
github.com/tklauser/go-sysconf v0.3.15 // indirect
8282
github.com/yusufpapurcu/wmi v1.2.4 // indirect

keystore/keystore.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"slices"
12+
"strings"
1213
"sync"
1314
"testing"
1415
"time"
@@ -25,6 +26,38 @@ import (
2526
"github.com/smartcontractkit/chainlink-common/keystore/serialization"
2627
)
2728

29+
type KeyPath []string
30+
31+
func (k KeyPath) String() string {
32+
return joinKeySegments(k...)
33+
}
34+
35+
func (k KeyPath) Base() string {
36+
return k[len(k)-1]
37+
}
38+
39+
func NewKeyPath(segments ...string) KeyPath {
40+
return segments
41+
}
42+
43+
func NewKeyPathFromString(fullName string) KeyPath {
44+
return strings.Split(fullName, "/")
45+
}
46+
47+
// joinKeySegments joins path-like key name segments using "/" and avoids double slashes.
48+
// Empty segments are skipped so joinKeySegments("EVM", "TX", "my-key") => "EVM/TX/my-key".
49+
func joinKeySegments(segments ...string) string {
50+
cleaned := make([]string, 0, len(segments))
51+
for _, s := range segments {
52+
s = strings.Trim(s, "/")
53+
if s == "" {
54+
continue
55+
}
56+
cleaned = append(cleaned, s)
57+
}
58+
return strings.Join(cleaned, "/")
59+
}
60+
2861
type KeyType string
2962

3063
func (k KeyType) String() string {

keystore/keystore_internal_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,23 @@ func TestPublicKeyFromPrivateKey(t *testing.T) {
3030
// We use SEC1 (uncompressed) format for ECDH public keys.
3131
require.Equal(t, 65, len(pubKey))
3232
}
33+
34+
func TestJoinKeySegments(t *testing.T) {
35+
tests := []struct {
36+
segments []string
37+
expected string
38+
}{
39+
{segments: []string{"EVM", "TX", "my-key"}, expected: "EVM/TX/my-key"},
40+
{segments: []string{"EVM", "/TX", "my-key"}, expected: "EVM/TX/my-key"},
41+
{segments: []string{"EVM", "TX/", "my-key"}, expected: "EVM/TX/my-key"},
42+
{segments: []string{"EVM", "TX", "/my-key"}, expected: "EVM/TX/my-key"},
43+
{segments: []string{"EVM", "TX", "my-key", ""}, expected: "EVM/TX/my-key"},
44+
{segments: []string{"EVM", "TX", "my-key", "/"}, expected: "EVM/TX/my-key"},
45+
{segments: []string{"EVM", "TX", "my-key", "//"}, expected: "EVM/TX/my-key"},
46+
{segments: []string{"EVM", "TX", "my-key", "///"}, expected: "EVM/TX/my-key"},
47+
{segments: []string{"EVM", "TX", "my-key", "////"}, expected: "EVM/TX/my-key"},
48+
}
49+
for _, tt := range tests {
50+
require.Equal(t, tt.expected, joinKeySegments(tt.segments...))
51+
}
52+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package ocr2offchain
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/smartcontractkit/chainlink-common/keystore"
9+
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
10+
"golang.org/x/crypto/curve25519"
11+
)
12+
13+
const (
14+
OCR2OffchainSigning = "ocr2_offchain_signing"
15+
OCR2OffchainEncryption = "ocr2_offchain_encryption"
16+
OCR2OffchainPrefix = "ocr2_offchain"
17+
)
18+
19+
func CreateOCR2OffchainKeyring(ctx context.Context, ks keystore.Keystore, keyringName string) (ocrtypes.OffchainKeyring, error) {
20+
signingKeyPath := keystore.NewKeyPath(OCR2OffchainPrefix, keyringName, OCR2OffchainSigning)
21+
encryptionKeyPath := keystore.NewKeyPath(OCR2OffchainPrefix, keyringName, OCR2OffchainEncryption)
22+
createReq := keystore.CreateKeysRequest{
23+
Keys: []keystore.CreateKeyRequest{
24+
{
25+
KeyName: signingKeyPath.String(),
26+
KeyType: keystore.Ed25519,
27+
},
28+
{
29+
KeyName: encryptionKeyPath.String(),
30+
KeyType: keystore.X25519,
31+
},
32+
},
33+
}
34+
resp, err := ks.CreateKeys(ctx, createReq)
35+
if err != nil {
36+
return nil, err
37+
}
38+
if len(resp.Keys) != 2 {
39+
return nil, fmt.Errorf("expected 2 keys, got %d", len(resp.Keys))
40+
}
41+
return &evmOffchainKeyring{
42+
ks: ks,
43+
signingKeyPath: signingKeyPath,
44+
encryptionKeyPath: encryptionKeyPath,
45+
offchainKey: resp.Keys[0].KeyInfo,
46+
offchainEncryptionKey: resp.Keys[1].KeyInfo,
47+
}, nil
48+
}
49+
50+
// ListOCR2OffchainKeyrings lists OCR2 offchain keyrings. If no local names provided, returns all OCR2 offchain keyrings.
51+
func GetOCR2OffchainKeyrings(ctx context.Context, ks keystore.Keystore, keyRingNames []string) ([]ocrtypes.OffchainKeyring, error) {
52+
var names []string
53+
if len(keyRingNames) > 0 {
54+
for _, keyRingName := range keyRingNames {
55+
names = append(names, keystore.NewKeyPath(OCR2OffchainPrefix, keyRingName, OCR2OffchainSigning).String())
56+
names = append(names, keystore.NewKeyPath(OCR2OffchainPrefix, keyRingName, OCR2OffchainEncryption).String())
57+
}
58+
}
59+
60+
getReq := keystore.GetKeysRequest{KeyNames: names}
61+
resp, err := ks.GetKeys(ctx, getReq)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
// Group by keyrings.
67+
keyRingMap := make(map[string][]keystore.KeyInfo)
68+
for _, key := range resp.Keys {
69+
if !strings.HasPrefix(key.KeyInfo.Name, keystore.NewKeyPath(OCR2OffchainPrefix).String()) {
70+
continue
71+
}
72+
keyPath := keystore.NewKeyPathFromString(key.KeyInfo.Name)
73+
// Example:
74+
// /ocr2_offchain/keyring_name/ocr2_offchain_signing
75+
// /ocr2_offchain/keyring_name/ocr2_offchain_encryption
76+
// Group by keyring name (first 3 segments: ocr2_offchain/keyring_name)
77+
keyRingMap[keyPath[:2].String()] = append(keyRingMap[keyPath[:2].String()], key.KeyInfo)
78+
}
79+
80+
var keyrings []ocrtypes.OffchainKeyring
81+
for _, keyInfos := range keyRingMap {
82+
// Find signing and encryption keys
83+
var signingKey, encryptionKey keystore.KeyInfo
84+
for _, keyInfo := range keyInfos {
85+
if strings.HasSuffix(keyInfo.Name, OCR2OffchainSigning) {
86+
signingKey = keyInfo
87+
} else if strings.HasSuffix(keyInfo.Name, OCR2OffchainEncryption) {
88+
encryptionKey = keyInfo
89+
}
90+
}
91+
keyrings = append(keyrings, &evmOffchainKeyring{
92+
ks: ks,
93+
signingKeyPath: keystore.NewKeyPathFromString(signingKey.Name),
94+
encryptionKeyPath: keystore.NewKeyPathFromString(encryptionKey.Name),
95+
offchainKey: signingKey,
96+
offchainEncryptionKey: encryptionKey,
97+
})
98+
}
99+
return keyrings, nil
100+
}
101+
102+
var _ ocrtypes.OffchainKeyring = &evmOffchainKeyring{}
103+
104+
type evmOffchainKeyring struct {
105+
ks keystore.Keystore
106+
signingKeyPath keystore.KeyPath
107+
encryptionKeyPath keystore.KeyPath
108+
offchainKey keystore.KeyInfo
109+
offchainEncryptionKey keystore.KeyInfo
110+
}
111+
112+
func (k *evmOffchainKeyring) ConfigEncryptionKeyPath() keystore.KeyPath {
113+
return k.encryptionKeyPath
114+
}
115+
116+
func (k *evmOffchainKeyring) ConfigSigningKeyPath() keystore.KeyPath {
117+
return k.signingKeyPath
118+
}
119+
120+
func (k *evmOffchainKeyring) OffchainPublicKey() ocrtypes.OffchainPublicKey {
121+
var pubKey ocrtypes.OffchainPublicKey
122+
copy(pubKey[:], k.offchainKey.PublicKey)
123+
return pubKey
124+
}
125+
126+
func (k *evmOffchainKeyring) ConfigEncryptionPublicKey() ocrtypes.ConfigEncryptionPublicKey {
127+
var pubKey ocrtypes.ConfigEncryptionPublicKey
128+
copy(pubKey[:], k.offchainEncryptionKey.PublicKey)
129+
return pubKey
130+
}
131+
132+
func (k *evmOffchainKeyring) OffchainSign(msg []byte) ([]byte, error) {
133+
signResp, err := k.ks.Sign(context.Background(), keystore.SignRequest{
134+
KeyName: k.offchainKey.Name,
135+
Data: msg,
136+
})
137+
return signResp.Signature, err
138+
}
139+
140+
func (k *evmOffchainKeyring) ConfigDiffieHellman(point [curve25519.PointSize]byte) ([curve25519.PointSize]byte, error) {
141+
resp, err := k.ks.DeriveSharedSecret(context.Background(), keystore.DeriveSharedSecretRequest{
142+
KeyName: k.offchainEncryptionKey.Name,
143+
RemotePubKey: point[:],
144+
})
145+
if err != nil {
146+
return [curve25519.PointSize]byte{}, err
147+
}
148+
149+
var sharedPoint [curve25519.PointSize]byte
150+
copy(sharedPoint[:], resp.SharedSecret)
151+
return sharedPoint, nil
152+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package ocr2offchain_test
2+
3+
import (
4+
"testing"
5+
6+
commonks "github.com/smartcontractkit/chainlink-common/keystore"
7+
"github.com/smartcontractkit/chainlink-common/keystore/ocr2offchain"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestOCR2OffchainKeyring(t *testing.T) {
12+
storage := commonks.NewMemoryStorage()
13+
ctx := t.Context()
14+
ks, err := commonks.LoadKeystore(ctx, storage, "test-password")
15+
require.NoError(t, err)
16+
keyring, err := ocr2offchain.CreateOCR2OffchainKeyring(ctx, ks, "test-ocr2-offchain-keyring")
17+
require.NoError(t, err)
18+
require.NotNil(t, keyring)
19+
20+
msg := []byte("test-message")
21+
signature, err := keyring.OffchainSign(msg)
22+
require.NoError(t, err)
23+
require.NotNil(t, signature)
24+
25+
keyrings, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, ks, []string{"test-ocr2-offchain-keyring"})
26+
require.NoError(t, err)
27+
require.Equal(t, 1, len(keyrings))
28+
require.Equal(t, keyring.OffchainPublicKey(), keyrings[0].OffchainPublicKey())
29+
require.Equal(t, keyring.ConfigEncryptionPublicKey(), keyrings[0].ConfigEncryptionPublicKey())
30+
31+
// List all works
32+
allKeyrings, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, ks, []string{})
33+
require.NoError(t, err)
34+
require.Equal(t, 1, len(allKeyrings))
35+
36+
// List non-existent errors.
37+
nonExistentKeyrings, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, ks, []string{"non-existent-ocr2-offchain-keyring"})
38+
require.Error(t, err)
39+
require.Nil(t, nonExistentKeyrings)
40+
41+
// Can create multiple.
42+
keyring2, err := ocr2offchain.CreateOCR2OffchainKeyring(ctx, ks, "test-ocr2-offchain-keyring-2")
43+
require.NoError(t, err)
44+
require.NotNil(t, keyring2)
45+
msg2 := []byte("test-message-2")
46+
signature2, err := keyring2.OffchainSign(msg2)
47+
require.NoError(t, err)
48+
require.NotNil(t, signature2)
49+
50+
// List by name works.
51+
keyrings2, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, ks, []string{"test-ocr2-offchain-keyring-2"})
52+
require.NoError(t, err)
53+
require.Equal(t, 1, len(keyrings2))
54+
require.Equal(t, keyring2.OffchainPublicKey(), keyrings2[0].OffchainPublicKey())
55+
require.Equal(t, keyring2.ConfigEncryptionPublicKey(), keyrings2[0].ConfigEncryptionPublicKey())
56+
57+
// List all works with multiple.
58+
allKeyrings2, err := ocr2offchain.GetOCR2OffchainKeyrings(ctx, ks, []string{})
59+
require.NoError(t, err)
60+
require.Equal(t, 2, len(allKeyrings2))
61+
62+
sig, err := allKeyrings[0].OffchainSign([]byte("test-message"))
63+
require.NoError(t, err)
64+
require.NotNil(t, sig)
65+
66+
pubkey := allKeyrings[0].OffchainPublicKey()
67+
valid, err := ks.Verify(ctx, commonks.VerifyRequest{
68+
KeyType: commonks.Ed25519,
69+
PublicKey: pubkey[:],
70+
Data: []byte("test-message"),
71+
Signature: sig,
72+
})
73+
require.NoError(t, err)
74+
require.True(t, valid.Valid)
75+
}

0 commit comments

Comments
 (0)