Skip to content

Commit 80a6415

Browse files
committed
Merge branch 'main' of https://github.com/smartcontractkit/chainlink-common into PLEX-1460-delivery-acks
2 parents dea279a + 31ad843 commit 80a6415

File tree

5 files changed

+509
-16
lines changed

5 files changed

+509
-16
lines changed

keystore/README.md

Lines changed: 187 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,189 @@
11
[![Go Reference](https://pkg.go.dev/badge/github.com/smartcontractkit/chainlink-common/keystore.svg)](https://pkg.go.dev/github.com/smartcontractkit/chainlink-common/keystore)
22

3-
WARNING: In development do not use in production.
4-
5-
# Keystore
6-
Design principles:
7-
- Use structs for typed extensibility of the interfaces. Easy
8-
to wrap via a network layer if needed.
9-
- Storage abstract. Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of
10-
contexts. Use write through caching to maintain synchronization between in memory keys and stored keys.
11-
- Only the Admin interface mutates the keystore, all other interfaces are read only. Admin interface
12-
is plural/batched to support atomic batched mutations.
13-
- Client side key naming. Keystore itself doesn't impose certain key algorithims/curves be used for specific contexts, it just supports a the minimum viable set of algorithms/curves for chainlink wide use cases. Clients define a name for each key which represents
14-
the context in which they wish to use it.
15-
- Common serialization/encryption for all storage types. Protobuf serialization (compact, versioned) for key material and then key material encrypted before persistence with a passphase.
16-
17-
Notes
18-
- keystore/internal is copied from https://github.com/smartcontractkit/chainlink/blob/develop/core/services/keystore/internal/raw.go#L3. Intention is to switch to core to use this library at which point we can remove the core copy.
3+
4+
## Keystore
5+
6+
Dependency minimized chain family agnostic key storage library. Supports the following key types:
7+
- Digital Signatures: ECDSA on secp256k1 and Ed25519.
8+
Note KMS is supported for these key types.
9+
- Hybrid Encryption: X25519 (nacl/box) and ECDH on P256
10+
11+
Warning: ECDH on P256 is pending audit do not use in
12+
production.
13+
14+
Family specific logic is layered on top in the following locations:
15+
- [EVM](https://github.com/smartcontractkit/chainlink-evm/tree/develop/pkg/keys/v2)
16+
- Note this also holds a full e2e example of using the keystore with [libocr](https://github.com/smartcontractkit/libocr/tree/master).
17+
- [Solana](https://github.com/smartcontractkit/chainlink-solana/tree/develop/pkg/solana/keys)
18+
- More coming soon
19+
20+
21+
### Examples
22+
23+
#### Signatures
24+
```go
25+
package main
26+
27+
import (
28+
"context"
29+
"crypto/sha256"
30+
31+
"github.com/smartcontractkit/chainlink-common/keystore"
32+
)
33+
34+
func main() {
35+
ctx := context.Background()
36+
37+
// Note postgres and file based storage also supported.
38+
ks, err := keystore.LoadKeystore(ctx, keystore.NewMemoryStorage(), "password")
39+
40+
createResp, _ := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
41+
Keys: []keystore.CreateKeyRequest{
42+
{KeyName: "my-key", KeyType: keystore.ECDSA_S256},
43+
},
44+
})
45+
// Sign data
46+
data := []byte("hello world")
47+
hash := sha256.Sum256(data)
48+
49+
signResp, _ := ks.Sign(ctx, keystore.SignRequest{
50+
KeyName: "my-key",
51+
Data: hash[:],
52+
})
53+
54+
// Verify the signature
55+
verifyResp, _ := ks.Verify(ctx, keystore.VerifyRequest{
56+
KeyType: keystore.ECDSA_S256,
57+
PublicKey: createResp.Keys[0].KeyInfo.PublicKey,
58+
Data: hash[:],
59+
Signature: signResp.Signature,
60+
})
61+
// verifyResp.Valid == true
62+
}
63+
```
64+
65+
#### KMS Signatures
66+
```go
67+
package main
68+
69+
import (
70+
"context"
71+
"crypto/sha256"
72+
73+
"github.com/smartcontractkit/chainlink-common/keystore"
74+
"github.com/smartcontractkit/chainlink-common/keystore/kms"
75+
)
76+
77+
func main() {
78+
ctx := context.Background()
79+
80+
// Create a KMS backed keystore.
81+
kmsClient, _ := kms.NewClient(ctx, kms.ClientOptions{
82+
Profile: "my-profile", // Optional: omit for default credential chain
83+
})
84+
ks, _ := kms.NewKeystore(kmsClient)
85+
86+
data := []byte("hello world")
87+
hash := sha256.Sum256(data)
88+
// Same signer interface but uses AWS allocated
89+
// key names.
90+
signResp, _ := ks.Sign(ctx, keystore.SignRequest{
91+
KeyName: "AWSkeyID",
92+
Data: hash[:],
93+
})
94+
95+
// Verify the signature
96+
verifyResp, _ := keystore.Verify(ctx, keystore.VerifyRequest{
97+
KeyType: keysResp.Keys[0].KeyInfo.KeyType,
98+
PublicKey: keysResp.Keys[0].KeyInfo.PublicKey,
99+
Data: hash[:],
100+
Signature: signResp.Signature,
101+
})
102+
// verifyResp.Valid == true
103+
}
104+
```
105+
106+
107+
#### Encryption
108+
```go
109+
package main
110+
111+
import (
112+
"context"
113+
114+
"github.com/smartcontractkit/chainlink-common/keystore"
115+
)
116+
117+
func main() {
118+
ctx := context.Background()
119+
120+
ks, _ := keystore.LoadKeystore(ctx, keystore.NewMemoryStorage(), "password")
121+
122+
// Create an X25519 key for encryption/decryption
123+
createResp, _ := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
124+
Keys: []keystore.CreateKeyRequest{
125+
{KeyName: "encrypt-key", KeyType: keystore.X25519},
126+
},
127+
})
128+
129+
// Encrypt data using the public key
130+
data := []byte("secret message")
131+
encryptResp, _ := ks.Encrypt(ctx, keystore.EncryptRequest{
132+
RemoteKeyType: keystore.X25519,
133+
RemotePubKey: createResp.Keys[0].KeyInfo.PublicKey,
134+
Data: data,
135+
})
136+
137+
// Decrypt using the key name
138+
decryptResp, _ := ks.Decrypt(ctx, keystore.DecryptRequest{
139+
KeyName: "encrypt-key",
140+
EncryptedData: encryptResp.EncryptedData,
141+
})
142+
// decryptResp.Data == data
143+
}
144+
```
145+
146+
147+
#### CLI
148+
```bash
149+
# Set up environment variables
150+
export KEYSTORE_PASSWORD="my-secure-password"
151+
export KEYSTORE_FILE_PATH="./keystore.json"
152+
153+
# Create a new keystore file (if using file storage)
154+
touch ./keystore.json
155+
156+
# Create an ECDSA key
157+
keys create -d '{"Keys": [{"KeyName": "my-key", "KeyType": "ECDSA_S256"}]}'
158+
159+
# List all keys
160+
keys list
161+
162+
# Get a specific key
163+
keys get -d '{"KeyNames": ["my-key"]}'
164+
165+
# Sign data (data must be base64-encoded, 32 bytes for ECDSA_S256)
166+
echo -n "hello world" | shasum -a 256 | cut -d' ' -f1 | xxd -r -p | base64
167+
# Use the output in the sign command:
168+
keys sign -d '{"KeyName": "my-key", "Data": "<base64-hash>"}'
169+
170+
# Verify a signature
171+
keys verify -d '{"KeyType": "ECDSA_S256", "PublicKey": "<base64-public-key>", "Data": "<base64-hash>", "Signature": "<base64-signature>"}'
172+
```
173+
174+
For KMS usage, set `KEYSTORE_KMS_PROFILE` instead:
175+
```bash
176+
export KEYSTORE_KMS_PROFILE="my-aws-profile"
177+
keys list # Lists KMS keys
178+
keys sign -d '{"KeyName": "arn:aws:kms:us-west-2:123456789012:key/abc123", "Data": "<base64-hash>"}'
179+
```
180+
181+
### Design Principles
182+
- **Embeddable CLI** The cli package is designed to support
183+
embedding in downstream applications so a consistent CLI
184+
can be shared across them.
185+
- **Typed extensibility**: Use structs for requests/responses that are extensible and easy to wrap via a network layer if needed.
186+
- **Storage abstract**: Keystore interfaces can be implemented with memory, file, database, etc. for storage to be useable in a variety of contexts. Uses write-through caching to maintain synchronization between in-memory keys and stored keys.
187+
- **Admin interface for mutations**: Only the Admin interface mutates the keystore; all other interfaces are read-only. Admin interface is plural/batched to support atomic batched mutations.
188+
- **Client side key naming**: Keystore doesn't impose specific key algorithms/curves for specific contexts. It supports a minimum viable set of algorithms/curves for chainlink-wide use cases. Clients define a name for each key representing the context in which they wish to use it.
189+
- **Common serialization/encryption**: Protobuf serialization (compact, versioned) for key material, then encrypted before persistence with a passphrase.

pkg/beholder/client.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,74 @@ func newOtelResource(cfg Config) (resource *sdkresource.Resource, err error) {
343343
return
344344
}
345345

346+
// RecordConfig records the beholder config as a metric.
347+
func (c *Client) RecordConfigMetric(ctx context.Context) error {
348+
configGauge, configAttrs, err := createConfigMetric(c.Meter, c.Config)
349+
if err != nil {
350+
return err
351+
}
352+
configGauge.Record(ctx, 1, otelmetric.WithAttributes(configAttrs...))
353+
return nil
354+
}
355+
356+
// createConfigMetric creates a configuration info metric with Beholder settings as attributes.
357+
func createConfigMetric(meter otelmetric.Meter, cfg Config) (otelmetric.Int64Gauge, []attribute.KeyValue, error) {
358+
configGauge, err := meter.Int64Gauge(
359+
"beholder.config.info",
360+
otelmetric.WithDescription("Beholder config info metric"),
361+
otelmetric.WithUnit("{info}"),
362+
)
363+
if err != nil {
364+
return nil, nil, fmt.Errorf("failed to create beholder config info metric: %w", err)
365+
}
366+
367+
configAttrs := []attribute.KeyValue{
368+
// Logging config
369+
attribute.Bool(
370+
"log_streaming_enabled", cfg.LogStreamingEnabled),
371+
attribute.String(
372+
"log_level", cfg.LogLevel.String()),
373+
attribute.Bool(
374+
"log_batch_processor", cfg.LogBatchProcessor),
375+
attribute.String(
376+
"log_export_interval", cfg.LogExportInterval.String()),
377+
attribute.Int(
378+
"log_export_max_batch_size", cfg.LogExportMaxBatchSize),
379+
attribute.Int(
380+
"log_max_queue_size", cfg.LogMaxQueueSize),
381+
attribute.String(
382+
"log_compressor", cfg.LogCompressor),
383+
384+
// Message emitter config
385+
attribute.Bool(
386+
"chip_ingress_enabled", cfg.ChipIngressEmitterEnabled),
387+
attribute.Bool(
388+
"emitter_batch_processor", cfg.EmitterBatchProcessor),
389+
attribute.String(
390+
"emitter_export_interval", cfg.EmitterExportInterval.String()),
391+
attribute.Int(
392+
"emitter_export_max_batch_size", cfg.EmitterExportMaxBatchSize),
393+
attribute.Int(
394+
"emitter_max_queue_size", cfg.EmitterMaxQueueSize),
395+
396+
// Tracing config
397+
attribute.Float64(
398+
"trace_sample_ratio", cfg.TraceSampleRatio),
399+
attribute.String(
400+
"trace_batch_timeout", cfg.TraceBatchTimeout.String()),
401+
attribute.String(
402+
"trace_compressor", cfg.TraceCompressor),
403+
404+
// Metrics config
405+
attribute.String(
406+
"metric_reader_interval", cfg.MetricReaderInterval.String()),
407+
attribute.String(
408+
"metric_compressor", cfg.MetricCompressor),
409+
}
410+
411+
return configGauge, configAttrs, nil
412+
}
413+
346414
type shutdowner interface {
347415
Shutdown(ctx context.Context) error
348416
}

pkg/capabilities/registry/base.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ func (a *atomicTriggerCapability) AckEvent(ctx context.Context, triggerId string
381381
return a.cap.AckEvent(ctx, triggerId, eventId)
382382
}
383383

384+
func (a *atomicTriggerCapability) Load() *capabilities.TriggerCapability {
385+
a.mu.RLock()
386+
defer a.mu.RUnlock()
387+
if a.cap == nil {
388+
return nil
389+
}
390+
cap := a.cap
391+
return &cap
392+
}
393+
384394
func (a *atomicTriggerCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) {
385395
a.mu.Lock()
386396
defer a.mu.Unlock()
@@ -442,6 +452,16 @@ func (a *atomicExecuteCapability) GetState() connectivity.State {
442452
return connectivity.State(-1) // unknown
443453
}
444454

455+
func (a *atomicExecuteCapability) Load() *capabilities.ExecutableCapability {
456+
a.mu.RLock()
457+
defer a.mu.RUnlock()
458+
if a.cap == nil {
459+
return nil
460+
}
461+
cap := a.cap
462+
return &cap
463+
}
464+
445465
func (a *atomicExecuteCapability) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error {
446466
a.mu.RLock()
447467
defer a.mu.RUnlock()
@@ -530,6 +550,16 @@ func (a *atomicExecuteAndTriggerCapability) AckEvent(ctx context.Context, trigge
530550
return a.cap.AckEvent(ctx, triggerId, eventId)
531551
}
532552

553+
func (a *atomicExecuteAndTriggerCapability) Load() *capabilities.ExecutableAndTriggerCapability {
554+
a.mu.RLock()
555+
defer a.mu.RUnlock()
556+
if a.cap == nil {
557+
return nil
558+
}
559+
cap := a.cap
560+
return &cap
561+
}
562+
533563
func (a *atomicExecuteAndTriggerCapability) RegisterTrigger(ctx context.Context, request capabilities.TriggerRegistrationRequest) (<-chan capabilities.TriggerResponse, error) {
534564
a.mu.Lock()
535565
defer a.mu.Unlock()

0 commit comments

Comments
 (0)