Skip to content

Commit 8c09813

Browse files
committed
CRE Don2Don accept OCR attestation of a response
1 parent 27bfcc7 commit 8c09813

21 files changed

Lines changed: 464 additions & 69 deletions

File tree

core/capabilities/launcher.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/Masterminds/semver/v3"
12+
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
1213
"github.com/smartcontractkit/libocr/ragep2p"
1314
ragetypes "github.com/smartcontractkit/libocr/ragep2p/types"
1415

@@ -483,7 +484,7 @@ func (w *launcher) addRemoteCapability(ctx context.Context, cid string, capabili
483484

484485
methodConfig := capabilityConfig.CapabilityMethodConfig
485486
if methodConfig != nil { // v2 capability - handle via CombinedClient
486-
errAdd := w.addRemoteCapabilityV2(ctx, capability.ID, methodConfig, myDON, remoteDON)
487+
errAdd := w.addRemoteCapabilityV2(ctx, capability.ID, methodConfig, myDON, remoteDON, capabilityConfig.Ocr3Configs)
487488
if errAdd != nil {
488489
return fmt.Errorf("failed to add remote v2 capability %s: %w", capability.ID, errAdd)
489490
}
@@ -574,7 +575,7 @@ func (w *launcher) addRemoteCapability(ctx context.Context, cid string, capabili
574575
w.cachedShims.executableClients[shimKey] = execCap
575576
}
576577
// V1 capabilities read transmission schedule from every request
577-
if errCfg := execCap.SetConfig(info, myDON.DON, defaultTargetRequestTimeout, nil); errCfg != nil {
578+
if errCfg := execCap.SetConfig(info, myDON.DON, defaultTargetRequestTimeout, nil, nil); errCfg != nil {
578579
return nil, fmt.Errorf("failed to set trigger config: %w", errCfg)
579580
}
580581
return execCap.(capabilityService), nil
@@ -600,7 +601,7 @@ func (w *launcher) addRemoteCapability(ctx context.Context, cid string, capabili
600601
w.cachedShims.executableClients[shimKey] = execCap
601602
}
602603
// V1 capabilities read transmission schedule from every request
603-
if errCfg := execCap.SetConfig(info, myDON.DON, defaultTargetRequestTimeout, nil); errCfg != nil {
604+
if errCfg := execCap.SetConfig(info, myDON.DON, defaultTargetRequestTimeout, nil, nil); errCfg != nil {
604605
return nil, fmt.Errorf("failed to set trigger config: %w", errCfg)
605606
}
606607
return execCap.(capabilityService), nil
@@ -907,7 +908,7 @@ func signersFor(don registrysyncer.DON, localRegistry *registrysyncer.LocalRegis
907908
}
908909

909910
// Add a V2 capability with multiple methods, using CombinedClient.
910-
func (w *launcher) addRemoteCapabilityV2(ctx context.Context, capID string, methodConfig map[string]capabilities.CapabilityMethodConfig, myDON registrysyncer.DON, remoteDON registrysyncer.DON) error {
911+
func (w *launcher) addRemoteCapabilityV2(ctx context.Context, capID string, methodConfig map[string]capabilities.CapabilityMethodConfig, myDON registrysyncer.DON, remoteDON registrysyncer.DON, capabilityOcr3Configs map[string]ocrtypes.ContractConfig) error {
911912
info, err := capabilities.NewRemoteCapabilityInfo(
912913
capID,
913914
capabilities.CapabilityTypeCombined,
@@ -962,7 +963,7 @@ func (w *launcher) addRemoteCapabilityV2(ctx context.Context, capID string, meth
962963
Schedule: transmission.EnumToString(config.RemoteExecutableConfig.TransmissionSchedule),
963964
DeltaStage: config.RemoteExecutableConfig.DeltaStage,
964965
}
965-
err := client.SetConfig(info, myDON.DON, config.RemoteExecutableConfig.RequestTimeout, transmissionConfig)
966+
err := client.SetConfig(info, myDON.DON, config.RemoteExecutableConfig.RequestTimeout, transmissionConfig, capabilityOcr3Configs)
966967
if err != nil {
967968
w.lggr.Errorw("failed to update client config", "capID", capID, "method", method, "error", err)
968969
continue

core/capabilities/remote/executable/client.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"sync/atomic"
99
"time"
1010

11+
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
12+
1113
commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
1214
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
1315
"github.com/smartcontractkit/chainlink-common/pkg/logger"
@@ -46,12 +48,14 @@ type dynamicConfig struct {
4648
requestTimeout time.Duration
4749
// Has to be set only for V2 capabilities. V1 capabilities read transmission schedule from every request.
4850
transmissionConfig *transmission.TransmissionConfig
51+
// Has to be set only for V2 capabilities using OCR.
52+
ocr3Configs map[string]ocrtypes.ContractConfig
4953
}
5054

5155
type Client interface {
5256
commoncap.ExecutableCapability
5357
Receive(ctx context.Context, msg *types.MessageBody)
54-
SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig) error
58+
SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, ocr3Configs map[string]ocrtypes.ContractConfig) error
5559
}
5660

5761
var _ Client = &client{}
@@ -78,7 +82,7 @@ func NewClient(capabilityID string, capMethodName string, dispatcher types.Dispa
7882

7983
// SetConfig sets the remote capability configuration dynamically
8084
// TransmissionConfig has to be set only for V2 capabilities. V1 capabilities read transmission schedule from every request.
81-
func (c *client) SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig) error {
85+
func (c *client) SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, ocr3Configs map[string]ocrtypes.ContractConfig) error {
8286
if remoteCapabilityInfo.ID == "" || remoteCapabilityInfo.ID != c.capabilityID {
8387
return fmt.Errorf("capability info provided does not match the client's capabilityID: %s != %s", remoteCapabilityInfo.ID, c.capabilityID)
8488
}
@@ -98,8 +102,9 @@ func (c *client) SetConfig(remoteCapabilityInfo commoncap.CapabilityInfo, localD
98102
localDONInfo: localDonInfo,
99103
requestTimeout: requestTimeout,
100104
transmissionConfig: transmissionConfig,
105+
ocr3Configs: ocr3Configs,
101106
})
102-
c.lggr.Infow("SetConfig", "remoteDONName", remoteCapabilityInfo.DON.Name, "remoteDONID", remoteCapabilityInfo.DON.ID, "requestTimeout", requestTimeout, "transmissionConfig", transmissionConfig)
107+
c.lggr.Infow("SetConfig", "remoteDONName", remoteCapabilityInfo.DON.Name, "remoteDONID", remoteCapabilityInfo.DON.ID, "requestTimeout", requestTimeout, "transmissionConfig", transmissionConfig, "ocr3Configs", ocr3Configs)
103108
return nil
104109
}
105110

@@ -234,7 +239,7 @@ func (c *client) Execute(ctx context.Context, capReq commoncap.CapabilityRequest
234239
}
235240

236241
req, err := request.NewClientExecuteRequest(ctx, c.lggr, capReq, cfg.remoteCapabilityInfo, cfg.localDONInfo, c.dispatcher,
237-
cfg.requestTimeout, cfg.transmissionConfig, c.capMethodName)
242+
cfg.requestTimeout, cfg.transmissionConfig, c.capMethodName, cfg.ocr3Configs)
238243
if err != nil {
239244
return commoncap.CapabilityResponse{}, fmt.Errorf("failed to create client request: %w", err)
240245
}

core/capabilities/remote/executable/client_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func testClient(t *testing.T, numWorkflowPeers int, workflowNodeResponseTimeout
243243
for i := range numWorkflowPeers {
244244
workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i])
245245
caller := executable.NewClient(capInfo.ID, "", workflowPeerDispatcher, lggr)
246-
err := caller.SetConfig(capInfo, workflowDonInfo, workflowNodeResponseTimeout, nil)
246+
err := caller.SetConfig(capInfo, workflowDonInfo, workflowNodeResponseTimeout, nil, nil)
247247
require.NoError(t, err)
248248
servicetest.Run(t, caller)
249249
broker.RegisterReceiverNode(workflowPeers[i], caller)
@@ -403,7 +403,7 @@ func TestClient_SetConfig(t *testing.T) {
403403
DeltaStage: 10 * time.Millisecond,
404404
}
405405

406-
err := client.SetConfig(validCapInfo, validDonInfo, validTimeout, transmissionConfig)
406+
err := client.SetConfig(validCapInfo, validDonInfo, validTimeout, transmissionConfig, nil)
407407
require.NoError(t, err)
408408

409409
// Verify config was set
@@ -418,7 +418,7 @@ func TestClient_SetConfig(t *testing.T) {
418418
CapabilityType: commoncap.CapabilityTypeAction,
419419
}
420420

421-
err := client.SetConfig(invalidCapInfo, validDonInfo, validTimeout, nil)
421+
err := client.SetConfig(invalidCapInfo, validDonInfo, validTimeout, nil, nil)
422422
require.Error(t, err)
423423
assert.Contains(t, err.Error(), "capability info provided does not match the client's capabilityID")
424424
assert.Contains(t, err.Error(), "different_capability@1.0.0 != test_capability@1.0.0")
@@ -431,15 +431,15 @@ func TestClient_SetConfig(t *testing.T) {
431431
F: 0,
432432
}
433433

434-
err := client.SetConfig(validCapInfo, invalidDonInfo, validTimeout, nil)
434+
err := client.SetConfig(validCapInfo, invalidDonInfo, validTimeout, nil, nil)
435435
require.Error(t, err)
436436
assert.Contains(t, err.Error(), "empty localDonInfo provided")
437437
})
438438

439439
t.Run("successful config update", func(t *testing.T) {
440440
// Set initial config
441441
initialTimeout := 10 * time.Second
442-
err := client.SetConfig(validCapInfo, validDonInfo, initialTimeout, nil)
442+
err := client.SetConfig(validCapInfo, validDonInfo, initialTimeout, nil, nil)
443443
require.NoError(t, err)
444444

445445
// Replace with new config
@@ -450,7 +450,7 @@ func TestClient_SetConfig(t *testing.T) {
450450
F: 1,
451451
}
452452

453-
err = client.SetConfig(validCapInfo, newDonInfo, newTimeout, nil)
453+
err = client.SetConfig(validCapInfo, newDonInfo, newTimeout, nil, nil)
454454
require.NoError(t, err)
455455

456456
// Verify the config was completely replaced
@@ -494,7 +494,7 @@ func TestClient_SetConfig_StartClose(t *testing.T) {
494494
})
495495

496496
t.Run("start succeeds after config set", func(t *testing.T) {
497-
require.NoError(t, client.SetConfig(validCapInfo, validDonInfo, validTimeout, nil))
497+
require.NoError(t, client.SetConfig(validCapInfo, validDonInfo, validTimeout, nil, nil))
498498
require.NoError(t, client.Start(ctx))
499499
require.NoError(t, client.Close())
500500
})
@@ -504,12 +504,12 @@ func TestClient_SetConfig_StartClose(t *testing.T) {
504504
freshClient := executable.NewClient(capabilityID, "execute", dispatcher, lggr)
505505

506506
// Set initial config and start
507-
require.NoError(t, freshClient.SetConfig(validCapInfo, validDonInfo, validTimeout, nil))
507+
require.NoError(t, freshClient.SetConfig(validCapInfo, validDonInfo, validTimeout, nil, nil))
508508
require.NoError(t, freshClient.Start(ctx))
509509

510510
// Update config while running
511511
validCapInfo.Description = "new description"
512-
require.NoError(t, freshClient.SetConfig(validCapInfo, validDonInfo, validTimeout, nil))
512+
require.NoError(t, freshClient.SetConfig(validCapInfo, validDonInfo, validTimeout, nil, nil))
513513

514514
// Verify config was updated
515515
info, err := freshClient.Info(ctx)

core/capabilities/remote/executable/endtoend_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func testRemoteExecutableCapability(ctx context.Context, t *testing.T, underlyin
309309
for i := range numWorkflowPeers {
310310
workflowPeerDispatcher := broker.NewDispatcherForNode(workflowPeers[i])
311311
workflowNode := executable.NewClient(capInfo.ID, "", workflowPeerDispatcher, lggr)
312-
err := workflowNode.SetConfig(capInfo, workflowDonInfo, workflowNodeTimeout, nil)
312+
err := workflowNode.SetConfig(capInfo, workflowDonInfo, workflowNodeTimeout, nil, nil)
313313
require.NoError(t, err)
314314
servicetest.Run(t, workflowNode)
315315
broker.RegisterReceiverNode(workflowPeers[i], workflowNode)

core/capabilities/remote/executable/request/client_request.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"sync"
1111
"time"
1212

13+
ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types"
1314
"google.golang.org/protobuf/proto"
1415

1516
ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types"
1617

18+
"github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key"
1719
"github.com/smartcontractkit/chainlink-common/pkg/beholder"
1820
commoncap "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
1921
"github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb"
@@ -43,6 +45,7 @@ type ClientRequest struct {
4345
totalErrorCount int
4446
responseReceived map[p2ptypes.PeerID]bool
4547
lggr logger.Logger
48+
ocr3Configs map[string]ocrtypes.ContractConfig
4649

4750
requiredIdenticalResponses int
4851
remoteNodeCount int
@@ -58,6 +61,7 @@ type ClientRequest struct {
5861
func NewClientExecuteRequest(ctx context.Context, lggr logger.Logger, req commoncap.CapabilityRequest,
5962
remoteCapabilityInfo commoncap.CapabilityInfo, localDonInfo commoncap.DON, dispatcher types.Dispatcher,
6063
requestTimeout time.Duration, transmissionConfig *transmission.TransmissionConfig, capMethodName string,
64+
ocr3Configs map[string]ocrtypes.ContractConfig,
6165
) (*ClientRequest, error) {
6266
rawRequest, err := proto.MarshalOptions{Deterministic: true}.Marshal(pb.CapabilityRequestToProto(req))
6367
if err != nil {
@@ -87,14 +91,15 @@ func NewClientExecuteRequest(ctx context.Context, lggr logger.Logger, req common
8791
}
8892

8993
lggr = logger.With(lggr, "requestId", requestID) // cap ID and method name included in the parent logger
90-
return newClientRequest(ctx, lggr, requestID, remoteCapabilityInfo, localDonInfo, dispatcher, requestTimeout, tc, types.MethodExecute, rawRequest, workflowExecutionID, req.Metadata.ReferenceID, capMethodName)
94+
return newClientRequest(ctx, lggr, requestID, remoteCapabilityInfo, localDonInfo, dispatcher, requestTimeout, tc, types.MethodExecute, rawRequest, workflowExecutionID, req.Metadata.ReferenceID, capMethodName, ocr3Configs)
9195
}
9296

9397
var defaultDelayMargin = 10 * time.Second
9498

9599
func newClientRequest(ctx context.Context, lggr logger.Logger, requestID string, remoteCapabilityInfo commoncap.CapabilityInfo,
96100
localDonInfo commoncap.DON, dispatcher types.Dispatcher, requestTimeout time.Duration,
97101
tc transmission.TransmissionConfig, methodType string, rawRequest []byte, workflowExecutionID string, stepRef string, capMethodName string,
102+
ocr3Configs map[string]ocrtypes.ContractConfig,
98103
) (*ClientRequest, error) {
99104
remoteCapabilityDonInfo := remoteCapabilityInfo.DON
100105
if remoteCapabilityDonInfo == nil {
@@ -200,6 +205,7 @@ func newClientRequest(ctx context.Context, lggr logger.Logger, requestID string,
200205
responseCh: make(chan clientResponse, 1),
201206
wg: &wg,
202207
lggr: lggr,
208+
ocr3Configs: ocr3Configs,
203209
}, nil
204210
}
205211

@@ -301,6 +307,32 @@ func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) err
301307
c.responseReceived[sender] = true
302308

303309
if msg.Error == types.Error_OK {
310+
resp, err := pb.UnmarshalCapabilityResponse(msg.Payload)
311+
if err != nil {
312+
return fmt.Errorf("failed to unmarshal capability response: %w", err)
313+
}
314+
315+
if resp.Metadata.OCRAttestation != nil {
316+
// Since signatures are provided switch to OCR based validation. It's enough to get 1 response with F+1 signatures
317+
// to be confident that the response is honest.
318+
err = c.verifyAttestation(resp)
319+
if err != nil {
320+
c.lggr.Errorw("failed to verify capability response OCR attestation", "peer", sender, "err", err, "requestID", c.id, "msgPayload", hex.EncodeToString(msg.Payload))
321+
return fmt.Errorf("failed to verify capability response OCR attestation: %w", err)
322+
}
323+
324+
rpt := resp.Metadata.Metering[0]
325+
rpt.Peer2PeerID = sender.String()
326+
var payload []byte
327+
payload, err = c.encodePayloadWithMetadata(msg, commoncap.ResponseMetadata{Metering: []commoncap.MeteringNodeDetail{rpt}})
328+
if err != nil {
329+
return fmt.Errorf("failed to encode payload with metadata: %w", err)
330+
}
331+
332+
c.sendResponse(clientResponse{Result: payload})
333+
return nil
334+
}
335+
304336
// metering reports per node are aggregated into a single array of values. for any single node message, the
305337
// metering values are extracted from the CapabilityResponse, added to an array, and the CapabilityResponse
306338
// is marshalled without the metering value to get the hash. each node could have a different metering value
@@ -359,6 +391,47 @@ func (c *ClientRequest) OnMessage(_ context.Context, msg *types.MessageBody) err
359391
return nil
360392
}
361393

394+
func (c *ClientRequest) verifyAttestation(resp commoncap.CapabilityResponse) error {
395+
if c.ocr3Configs == nil {
396+
return errors.New("OCR3 configs not provided, cannot verify signatures")
397+
}
398+
399+
cfg, ok := c.ocr3Configs[pb.OCR3ConfigDefaultKey]
400+
if !ok {
401+
return fmt.Errorf("OCR3 config with key %s not found", pb.OCR3ConfigDefaultKey)
402+
}
403+
404+
attestation := resp.Metadata.OCRAttestation
405+
if len(attestation.Sigs) < int(cfg.F)+1 {
406+
return fmt.Errorf("not enough signatures: got %d, need at least %d", len(attestation.Sigs), cfg.F+1)
407+
}
408+
409+
if len(resp.Metadata.Metering) != 1 {
410+
return fmt.Errorf("unexpected number of metering records: got %d, want 1", len(resp.Metadata.Metering))
411+
}
412+
413+
reportData := commoncap.ResponseToReportData(c.id, resp.Payload.Value, resp.Metadata.Metering[0].SpendUnit, resp.Metadata.Metering[0].SpendValue)
414+
sigData := ocr2key.ReportToSigData3(attestation.ConfigDigest, attestation.SequenceNumber, reportData)
415+
signed := make([]bool, len(cfg.Signers))
416+
for _, sig := range attestation.Sigs {
417+
if int(sig.Signer) > len(cfg.Signers) {
418+
return fmt.Errorf("invalid signer index: %d", sig.Signer)
419+
}
420+
421+
if signed[sig.Signer] {
422+
return fmt.Errorf("duplicate signature from signer index: %d", sig.Signer)
423+
}
424+
425+
if !ocr2key.EvmVerifyBlob(cfg.Signers[sig.Signer], sigData, sig.Signature) {
426+
return fmt.Errorf("invalid signature from signer index: %d", sig.Signer)
427+
}
428+
429+
signed[sig.Signer] = true
430+
}
431+
432+
return nil
433+
}
434+
362435
func (c *ClientRequest) sendResponse(response clientResponse) {
363436
c.responseCh <- response
364437
close(c.responseCh)

0 commit comments

Comments
 (0)