diff --git a/keystore/corekeys/ocr2key/evm_keyring.go b/keystore/corekeys/ocr2key/evm_keyring.go index 4121b8c620..b953a81755 100644 --- a/keystore/corekeys/ocr2key/evm_keyring.go +++ b/keystore/corekeys/ocr2key/evm_keyring.go @@ -87,13 +87,7 @@ func (ekr *evmKeyring) Verify3(publicKey ocrtypes.OnchainPublicKey, cd ocrtypes. } func (ekr *evmKeyring) VerifyBlob(pubkey types.OnchainPublicKey, b, sig []byte) bool { - authorPubkey, err := crypto.SigToPub(b, sig) - if err != nil { - return false - } - authorAddress := crypto.PubkeyToAddress(*authorPubkey) - // no need for constant time compare since neither arg is sensitive - return bytes.Equal(pubkey[:], authorAddress[:]) + return EvmVerifyBlob(pubkey, b, sig) } func (ekr *evmKeyring) MaxSignatureLength() int { @@ -116,3 +110,13 @@ func (ekr *evmKeyring) Unmarshal(in []byte) error { ekr.privateKey = func() *ecdsa.PrivateKey { return privateKey } return nil } + +func EvmVerifyBlob(pubkey types.OnchainPublicKey, b, sig []byte) bool { + authorPubkey, err := crypto.SigToPub(b, sig) + if err != nil { + return false + } + authorAddress := crypto.PubkeyToAddress(*authorPubkey) + // no need for constant time compare since neither arg is sensitive + return bytes.Equal(pubkey[:], authorAddress[:]) +} diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go index ea3e0fc6d8..ed4b793534 100644 --- a/pkg/capabilities/capabilities.go +++ b/pkg/capabilities/capabilities.go @@ -2,6 +2,7 @@ package capabilities import ( "context" + "encoding/binary" "errors" "fmt" "iter" @@ -10,6 +11,7 @@ import ( "strings" "time" + "golang.org/x/crypto/sha3" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" @@ -39,6 +41,21 @@ func (e errStopExecution) Is(err error) bool { return strings.Contains(err.Error(), errStopExecutionMsg) } +// ErrResponsePayloadNotAvailable is returned when a capability's Execute method cannot provide a response payload and engine should wait for another response instead of treating it as an error. +var ErrResponsePayloadNotAvailable = &responsePayloadNotAvailableError{} + +type responsePayloadNotAvailableError struct{} + +const errResponsePayloadNotAvailableMsg = "__response_payload_not_available" + +func (e responsePayloadNotAvailableError) Error() string { + return errResponsePayloadNotAvailableMsg +} + +func (e responsePayloadNotAvailableError) Is(err error) bool { + return strings.Contains(err.Error(), errResponsePayloadNotAvailableMsg) +} + // CapabilityType enum values. const ( CapabilityTypeUnknown CapabilityType = "unknown" @@ -76,7 +93,8 @@ type CapabilityResponse struct { Metadata ResponseMetadata // Payload is used for no DAG workflows - Payload *anypb.Any + Payload *anypb.Any + OCRAttestation *OCRAttestation } type ResponseMetadata struct { @@ -84,6 +102,54 @@ type ResponseMetadata struct { CapDON_N uint32 } +type OCRAttestation struct { + ConfigDigest ocrtypes.ConfigDigest + SequenceNumber uint64 + Sigs []AttributedSignature +} + +type AttributedSignature struct { + Signature []byte + Signer uint32 +} + +func ExtractMeteringFromMetadata(sender p2ptypes.PeerID, metadata ResponseMetadata) (MeteringNodeDetail, error) { + if len(metadata.Metering) != 1 { + return MeteringNodeDetail{}, fmt.Errorf("unexpected number of metering records received from peer %s: got %d, want 1", sender, len(metadata.Metering)) + } + + rpt := metadata.Metering[0] + rpt.Peer2PeerID = sender.String() + return rpt, nil +} + +func ResponseToReportData(workflowExecutionID, referenceID string, responsePayload []byte, metadata ResponseMetadata) ([32]byte, error) { + // use empty PeerID since the sender must not be included in the hash + metering, err := ExtractMeteringFromMetadata(p2ptypes.PeerID{}, metadata) + if err != nil { + return [32]byte{}, fmt.Errorf("failed to extract metering from metadata: %w", err) + } + + hash := sha3.New256() + const domainSeparator = "CapabilityResponseReportData:v1" + hash.Write([]byte(domainSeparator)) + // Helper to write a length-prefixed byte slice. + writeField := func(b []byte) { + // Use a fixed-width length prefix to make encoding unambiguous. + _ = binary.Write(hash, binary.BigEndian, uint64(len(b))) + _, _ = hash.Write(b) + } + writeField([]byte(workflowExecutionID)) + writeField([]byte(referenceID)) + writeField(responsePayload) + writeField([]byte(metering.SpendUnit)) + writeField([]byte(metering.SpendValue)) + + var result [32]byte + copy(result[:], hash.Sum(nil)) + return result, nil +} + type MeteringNodeDetail struct { Peer2PeerID string SpendUnit string diff --git a/pkg/capabilities/capabilities_test.go b/pkg/capabilities/capabilities_test.go index 06e3bac43b..272622b535 100644 --- a/pkg/capabilities/capabilities_test.go +++ b/pkg/capabilities/capabilities_test.go @@ -3,6 +3,7 @@ package capabilities import ( "bytes" "context" + "encoding/hex" "errors" "maps" "strings" @@ -360,3 +361,113 @@ func TestRegistrationMetadata_ContextWithCRE(t *testing.T) { ctx = md.ContextWithCRE(ctx) require.Equal(t, "org-id", contexts.CREValue(ctx).Org) } + +func testResponseMetadata(spendUnit, spendValue string) ResponseMetadata { + return ResponseMetadata{ + Metering: []MeteringNodeDetail{{SpendUnit: spendUnit, SpendValue: spendValue}}, + } +} + +func TestResponseToReportData(t *testing.T) { + t.Run("deterministic golden digest", func(t *testing.T) { + // SHA3-256 over domain "CapabilityResponseReportData:v1" plus length-prefixed fields. + want, err := hex.DecodeString("5d39c100ecf57f83b095cc9e129e0be24809e208af97a40e6447932749272a50") + require.NoError(t, err) + require.Len(t, want, 32) + + got, err := ResponseToReportData( + "wf-exec", + "ref-1", + []byte("payload"), + testResponseMetadata("unit", "42"), + ) + require.NoError(t, err) + assert.Equal(t, want, got[:]) + }) + + t.Run("same inputs yield same hash", func(t *testing.T) { + md := testResponseMetadata("u", "1") + a, err := ResponseToReportData("wf", "ref", []byte("p"), md) + require.NoError(t, err) + b, err := ResponseToReportData("wf", "ref", []byte("p"), md) + require.NoError(t, err) + assert.Equal(t, a, b) + }) + + t.Run("nil payload matches empty payload", func(t *testing.T) { + md := testResponseMetadata("u", "v") + a, err := ResponseToReportData("w", "r", nil, md) + require.NoError(t, err) + b, err := ResponseToReportData("w", "r", []byte{}, md) + require.NoError(t, err) + assert.Equal(t, a, b) + }) + + t.Run("each field affects the digest", func(t *testing.T) { + base, err := ResponseToReportData( + "workflow-exec-id", + "reference-id", + []byte{0x01, 0x02}, + testResponseMetadata("spend-unit", "spend-value"), + ) + require.NoError(t, err) + + other, err := ResponseToReportData( + "other-workflow", + "reference-id", + []byte{0x01, 0x02}, + testResponseMetadata("spend-unit", "spend-value"), + ) + require.NoError(t, err) + assert.NotEqual(t, base, other) + + other, err = ResponseToReportData( + "workflow-exec-id", + "other-ref", + []byte{0x01, 0x02}, + testResponseMetadata("spend-unit", "spend-value"), + ) + require.NoError(t, err) + assert.NotEqual(t, base, other) + + other, err = ResponseToReportData( + "workflow-exec-id", + "reference-id", + []byte{0x01}, + testResponseMetadata("spend-unit", "spend-value"), + ) + require.NoError(t, err) + assert.NotEqual(t, base, other) + + other, err = ResponseToReportData( + "workflow-exec-id", + "reference-id", + []byte{0x01, 0x02}, + testResponseMetadata("other-unit", "spend-value"), + ) + require.NoError(t, err) + assert.NotEqual(t, base, other) + + other, err = ResponseToReportData( + "workflow-exec-id", + "reference-id", + []byte{0x01, 0x02}, + testResponseMetadata("spend-unit", "other-value"), + ) + require.NoError(t, err) + assert.NotEqual(t, base, other) + }) + + t.Run("metering must contain exactly one entry", func(t *testing.T) { + _, err := ResponseToReportData("w", "r", nil, ResponseMetadata{}) + require.ErrorContains(t, err, "failed to extract metering from metadata: unexpected number of metering records received from peer 12D3KooW9pNAk8aiBuGVQtWRdbkLmo5qVL3e2h5UxbN2Nz9ttwiw: got 0, want 1") + + _, err = ResponseToReportData("w", "r", nil, ResponseMetadata{ + Metering: []MeteringNodeDetail{ + {SpendUnit: "a", SpendValue: "1"}, + {SpendUnit: "b", SpendValue: "2"}, + }, + }) + require.ErrorContains(t, err, "failed to extract metering from metadata: unexpected number of metering records received from peer 12D3KooW9pNAk8aiBuGVQtWRdbkLmo5qVL3e2h5UxbN2Nz9ttwiw: got 2, want 1") + }) +} diff --git a/pkg/capabilities/pb/capabilities.pb.go b/pkg/capabilities/pb/capabilities.pb.go index a1c731f861..cc0ff27775 100644 --- a/pkg/capabilities/pb/capabilities.pb.go +++ b/pkg/capabilities/pb/capabilities.pb.go @@ -815,9 +815,10 @@ type CapabilityResponse struct { Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` Metadata *ResponseMetadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` // Used for no DAG SDK - Payload *anypb.Any `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Payload *anypb.Any `protobuf:"bytes,4,opt,name=payload,proto3" json:"payload,omitempty"` + OcrAttestation *OCRAttestation `protobuf:"bytes,5,opt,name=ocr_attestation,json=ocrAttestation,proto3,oneof" json:"ocr_attestation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilityResponse) Reset() { @@ -878,6 +879,13 @@ func (x *CapabilityResponse) GetPayload() *anypb.Any { return nil } +func (x *CapabilityResponse) GetOcrAttestation() *OCRAttestation { + if x != nil { + return x.OcrAttestation + } + return nil +} + type ResponseMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` // MeteringReportNodeDetail is repeated here due to @@ -940,6 +948,118 @@ func (x *ResponseMetadata) GetCapdonN() uint32 { return 0 } +type OCRAttestation struct { + state protoimpl.MessageState `protogen:"open.v1"` + ConfigDigest []byte `protobuf:"bytes,1,opt,name=config_digest,json=configDigest,proto3" json:"config_digest,omitempty"` + SequenceNumber uint64 `protobuf:"varint,2,opt,name=sequence_number,json=sequenceNumber,proto3" json:"sequence_number,omitempty"` + Signatures []*AttributedSignature `protobuf:"bytes,3,rep,name=signatures,proto3" json:"signatures,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OCRAttestation) Reset() { + *x = OCRAttestation{} + mi := &file_capabilities_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OCRAttestation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OCRAttestation) ProtoMessage() {} + +func (x *OCRAttestation) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OCRAttestation.ProtoReflect.Descriptor instead. +func (*OCRAttestation) Descriptor() ([]byte, []int) { + return file_capabilities_proto_rawDescGZIP(), []int{11} +} + +func (x *OCRAttestation) GetConfigDigest() []byte { + if x != nil { + return x.ConfigDigest + } + return nil +} + +func (x *OCRAttestation) GetSequenceNumber() uint64 { + if x != nil { + return x.SequenceNumber + } + return 0 +} + +func (x *OCRAttestation) GetSignatures() []*AttributedSignature { + if x != nil { + return x.Signatures + } + return nil +} + +type AttributedSignature struct { + state protoimpl.MessageState `protogen:"open.v1"` + Signature []byte `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` + Signer uint32 `protobuf:"varint,2,opt,name=signer,proto3" json:"signer,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttributedSignature) Reset() { + *x = AttributedSignature{} + mi := &file_capabilities_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttributedSignature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributedSignature) ProtoMessage() {} + +func (x *AttributedSignature) ProtoReflect() protoreflect.Message { + mi := &file_capabilities_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributedSignature.ProtoReflect.Descriptor instead. +func (*AttributedSignature) Descriptor() ([]byte, []int) { + return file_capabilities_proto_rawDescGZIP(), []int{12} +} + +func (x *AttributedSignature) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *AttributedSignature) GetSigner() uint32 { + if x != nil { + return x.Signer + } + return 0 +} + type RegistrationMetadata struct { state protoimpl.MessageState `protogen:"open.v1"` WorkflowId string `protobuf:"bytes,1,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` @@ -951,7 +1071,7 @@ type RegistrationMetadata struct { func (x *RegistrationMetadata) Reset() { *x = RegistrationMetadata{} - mi := &file_capabilities_proto_msgTypes[11] + mi := &file_capabilities_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -963,7 +1083,7 @@ func (x *RegistrationMetadata) String() string { func (*RegistrationMetadata) ProtoMessage() {} func (x *RegistrationMetadata) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[11] + mi := &file_capabilities_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -976,7 +1096,7 @@ func (x *RegistrationMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use RegistrationMetadata.ProtoReflect.Descriptor instead. func (*RegistrationMetadata) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{11} + return file_capabilities_proto_rawDescGZIP(), []int{13} } func (x *RegistrationMetadata) GetWorkflowId() string { @@ -1010,7 +1130,7 @@ type RegisterToWorkflowRequest struct { func (x *RegisterToWorkflowRequest) Reset() { *x = RegisterToWorkflowRequest{} - mi := &file_capabilities_proto_msgTypes[12] + mi := &file_capabilities_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1022,7 +1142,7 @@ func (x *RegisterToWorkflowRequest) String() string { func (*RegisterToWorkflowRequest) ProtoMessage() {} func (x *RegisterToWorkflowRequest) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[12] + mi := &file_capabilities_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1035,7 +1155,7 @@ func (x *RegisterToWorkflowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterToWorkflowRequest.ProtoReflect.Descriptor instead. func (*RegisterToWorkflowRequest) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{12} + return file_capabilities_proto_rawDescGZIP(), []int{14} } func (x *RegisterToWorkflowRequest) GetMetadata() *RegistrationMetadata { @@ -1062,7 +1182,7 @@ type UnregisterFromWorkflowRequest struct { func (x *UnregisterFromWorkflowRequest) Reset() { *x = UnregisterFromWorkflowRequest{} - mi := &file_capabilities_proto_msgTypes[13] + mi := &file_capabilities_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1074,7 +1194,7 @@ func (x *UnregisterFromWorkflowRequest) String() string { func (*UnregisterFromWorkflowRequest) ProtoMessage() {} func (x *UnregisterFromWorkflowRequest) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[13] + mi := &file_capabilities_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1087,7 +1207,7 @@ func (x *UnregisterFromWorkflowRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnregisterFromWorkflowRequest.ProtoReflect.Descriptor instead. func (*UnregisterFromWorkflowRequest) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{13} + return file_capabilities_proto_rawDescGZIP(), []int{15} } func (x *UnregisterFromWorkflowRequest) GetMetadata() *RegistrationMetadata { @@ -1125,7 +1245,7 @@ type InitialiseRequest struct { func (x *InitialiseRequest) Reset() { *x = InitialiseRequest{} - mi := &file_capabilities_proto_msgTypes[14] + mi := &file_capabilities_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1137,7 +1257,7 @@ func (x *InitialiseRequest) String() string { func (*InitialiseRequest) ProtoMessage() {} func (x *InitialiseRequest) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[14] + mi := &file_capabilities_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1150,7 +1270,7 @@ func (x *InitialiseRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InitialiseRequest.ProtoReflect.Descriptor instead. func (*InitialiseRequest) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{14} + return file_capabilities_proto_rawDescGZIP(), []int{16} } func (x *InitialiseRequest) GetConfig() string { @@ -1253,7 +1373,7 @@ type CapabilityInfosReply struct { func (x *CapabilityInfosReply) Reset() { *x = CapabilityInfosReply{} - mi := &file_capabilities_proto_msgTypes[15] + mi := &file_capabilities_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1265,7 +1385,7 @@ func (x *CapabilityInfosReply) String() string { func (*CapabilityInfosReply) ProtoMessage() {} func (x *CapabilityInfosReply) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[15] + mi := &file_capabilities_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1278,7 +1398,7 @@ func (x *CapabilityInfosReply) ProtoReflect() protoreflect.Message { // Deprecated: Use CapabilityInfosReply.ProtoReflect.Descriptor instead. func (*CapabilityInfosReply) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{15} + return file_capabilities_proto_rawDescGZIP(), []int{17} } func (x *CapabilityInfosReply) GetInfos() []*CapabilityInfoReply { @@ -1298,7 +1418,7 @@ type SettingsUpdate struct { func (x *SettingsUpdate) Reset() { *x = SettingsUpdate{} - mi := &file_capabilities_proto_msgTypes[16] + mi := &file_capabilities_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1310,7 +1430,7 @@ func (x *SettingsUpdate) String() string { func (*SettingsUpdate) ProtoMessage() {} func (x *SettingsUpdate) ProtoReflect() protoreflect.Message { - mi := &file_capabilities_proto_msgTypes[16] + mi := &file_capabilities_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1323,7 +1443,7 @@ func (x *SettingsUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use SettingsUpdate.ProtoReflect.Descriptor instead. func (*SettingsUpdate) Descriptor() ([]byte, []int) { - return file_capabilities_proto_rawDescGZIP(), []int{16} + return file_capabilities_proto_rawDescGZIP(), []int{18} } func (x *SettingsUpdate) GetSettings() string { @@ -1405,15 +1525,26 @@ const file_capabilities_proto_rawDesc = "" + "\n" + "trigger_id\x18\x01 \x01(\tR\ttriggerId\x12\x19\n" + "\bevent_id\x18\x02 \x01(\tR\aeventId\x12\x16\n" + - "\x06method\x18\x03 \x01(\tR\x06method\"\xbc\x01\n" + + "\x06method\x18\x03 \x01(\tR\x06method\"\x9c\x02\n" + "\x12CapabilityResponse\x12$\n" + "\x05value\x18\x01 \x01(\v2\x0e.values.v1.MapR\x05value\x12\x14\n" + "\x05error\x18\x02 \x01(\tR\x05error\x12:\n" + "\bmetadata\x18\x03 \x01(\v2\x1e.capabilities.ResponseMetadataR\bmetadata\x12.\n" + - "\apayload\x18\x04 \x01(\v2\x14.google.protobuf.AnyR\apayload\"m\n" + + "\apayload\x18\x04 \x01(\v2\x14.google.protobuf.AnyR\apayload\x12J\n" + + "\x0focr_attestation\x18\x05 \x01(\v2\x1c.capabilities.OCRAttestationH\x00R\x0eocrAttestation\x88\x01\x01B\x12\n" + + "\x10_ocr_attestation\"m\n" + "\x10ResponseMetadata\x12>\n" + "\bmetering\x18\x01 \x03(\v2\".metering.MeteringReportNodeDetailR\bmetering\x12\x19\n" + - "\bcapdon_n\x18\x02 \x01(\rR\acapdonN\"\x81\x01\n" + + "\bcapdon_n\x18\x02 \x01(\rR\acapdonN\"\xa1\x01\n" + + "\x0eOCRAttestation\x12#\n" + + "\rconfig_digest\x18\x01 \x01(\fR\fconfigDigest\x12'\n" + + "\x0fsequence_number\x18\x02 \x01(\x04R\x0esequenceNumber\x12A\n" + + "\n" + + "signatures\x18\x03 \x03(\v2!.capabilities.AttributedSignatureR\n" + + "signatures\"K\n" + + "\x13AttributedSignature\x12\x1c\n" + + "\tsignature\x18\x01 \x01(\fR\tsignature\x12\x16\n" + + "\x06signer\x18\x02 \x01(\rR\x06signer\"\x81\x01\n" + "\x14RegistrationMetadata\x12\x1f\n" + "\vworkflow_id\x18\x01 \x01(\tR\n" + "workflowId\x12!\n" + @@ -1485,7 +1616,7 @@ func file_capabilities_proto_rawDescGZIP() []byte { } var file_capabilities_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_capabilities_proto_msgTypes = make([]protoimpl.MessageInfo, 19) var file_capabilities_proto_goTypes = []any{ (CapabilityType)(0), // 0: capabilities.CapabilityType (*CapabilityInfoReply)(nil), // 1: capabilities.CapabilityInfoReply @@ -1499,69 +1630,73 @@ var file_capabilities_proto_goTypes = []any{ (*AckEventRequest)(nil), // 9: capabilities.AckEventRequest (*CapabilityResponse)(nil), // 10: capabilities.CapabilityResponse (*ResponseMetadata)(nil), // 11: capabilities.ResponseMetadata - (*RegistrationMetadata)(nil), // 12: capabilities.RegistrationMetadata - (*RegisterToWorkflowRequest)(nil), // 13: capabilities.RegisterToWorkflowRequest - (*UnregisterFromWorkflowRequest)(nil), // 14: capabilities.UnregisterFromWorkflowRequest - (*InitialiseRequest)(nil), // 15: capabilities.InitialiseRequest - (*CapabilityInfosReply)(nil), // 16: capabilities.CapabilityInfosReply - (*SettingsUpdate)(nil), // 17: capabilities.SettingsUpdate - (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp - (*pb.Map)(nil), // 19: values.v1.Map - (*anypb.Any)(nil), // 20: google.protobuf.Any - (*emptypb.Empty)(nil), // 21: google.protobuf.Empty - (*pb1.MeteringReportNodeDetail)(nil), // 22: metering.MeteringReportNodeDetail + (*OCRAttestation)(nil), // 12: capabilities.OCRAttestation + (*AttributedSignature)(nil), // 13: capabilities.AttributedSignature + (*RegistrationMetadata)(nil), // 14: capabilities.RegistrationMetadata + (*RegisterToWorkflowRequest)(nil), // 15: capabilities.RegisterToWorkflowRequest + (*UnregisterFromWorkflowRequest)(nil), // 16: capabilities.UnregisterFromWorkflowRequest + (*InitialiseRequest)(nil), // 17: capabilities.InitialiseRequest + (*CapabilityInfosReply)(nil), // 18: capabilities.CapabilityInfosReply + (*SettingsUpdate)(nil), // 19: capabilities.SettingsUpdate + (*timestamppb.Timestamp)(nil), // 20: google.protobuf.Timestamp + (*pb.Map)(nil), // 21: values.v1.Map + (*anypb.Any)(nil), // 22: google.protobuf.Any + (*emptypb.Empty)(nil), // 23: google.protobuf.Empty + (*pb1.MeteringReportNodeDetail)(nil), // 24: metering.MeteringReportNodeDetail } var file_capabilities_proto_depIdxs = []int32{ 0, // 0: capabilities.CapabilityInfoReply.capability_type:type_name -> capabilities.CapabilityType 2, // 1: capabilities.RequestMetadata.spend_limits:type_name -> capabilities.SpendLimit - 18, // 2: capabilities.RequestMetadata.execution_timestamp:type_name -> google.protobuf.Timestamp + 20, // 2: capabilities.RequestMetadata.execution_timestamp:type_name -> google.protobuf.Timestamp 3, // 3: capabilities.CapabilityRequest.metadata:type_name -> capabilities.RequestMetadata - 19, // 4: capabilities.CapabilityRequest.config:type_name -> values.v1.Map - 19, // 5: capabilities.CapabilityRequest.inputs:type_name -> values.v1.Map - 20, // 6: capabilities.CapabilityRequest.payload:type_name -> google.protobuf.Any - 20, // 7: capabilities.CapabilityRequest.configPayload:type_name -> google.protobuf.Any + 21, // 4: capabilities.CapabilityRequest.config:type_name -> values.v1.Map + 21, // 5: capabilities.CapabilityRequest.inputs:type_name -> values.v1.Map + 22, // 6: capabilities.CapabilityRequest.payload:type_name -> google.protobuf.Any + 22, // 7: capabilities.CapabilityRequest.configPayload:type_name -> google.protobuf.Any 3, // 8: capabilities.TriggerRegistrationRequest.metadata:type_name -> capabilities.RequestMetadata - 19, // 9: capabilities.TriggerRegistrationRequest.config:type_name -> values.v1.Map - 20, // 10: capabilities.TriggerRegistrationRequest.payload:type_name -> google.protobuf.Any - 19, // 11: capabilities.TriggerEvent.outputs:type_name -> values.v1.Map - 20, // 12: capabilities.TriggerEvent.payload:type_name -> google.protobuf.Any + 21, // 9: capabilities.TriggerRegistrationRequest.config:type_name -> values.v1.Map + 22, // 10: capabilities.TriggerRegistrationRequest.payload:type_name -> google.protobuf.Any + 21, // 11: capabilities.TriggerEvent.outputs:type_name -> values.v1.Map + 22, // 12: capabilities.TriggerEvent.payload:type_name -> google.protobuf.Any 6, // 13: capabilities.TriggerResponse.event:type_name -> capabilities.TriggerEvent - 21, // 14: capabilities.TriggerResponseMessage.ack:type_name -> google.protobuf.Empty + 23, // 14: capabilities.TriggerResponseMessage.ack:type_name -> google.protobuf.Empty 7, // 15: capabilities.TriggerResponseMessage.response:type_name -> capabilities.TriggerResponse - 19, // 16: capabilities.CapabilityResponse.value:type_name -> values.v1.Map + 21, // 16: capabilities.CapabilityResponse.value:type_name -> values.v1.Map 11, // 17: capabilities.CapabilityResponse.metadata:type_name -> capabilities.ResponseMetadata - 20, // 18: capabilities.CapabilityResponse.payload:type_name -> google.protobuf.Any - 22, // 19: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail - 12, // 20: capabilities.RegisterToWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata - 19, // 21: capabilities.RegisterToWorkflowRequest.config:type_name -> values.v1.Map - 12, // 22: capabilities.UnregisterFromWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata - 19, // 23: capabilities.UnregisterFromWorkflowRequest.config:type_name -> values.v1.Map - 1, // 24: capabilities.CapabilityInfosReply.infos:type_name -> capabilities.CapabilityInfoReply - 21, // 25: capabilities.BaseCapability.Info:input_type -> google.protobuf.Empty - 5, // 26: capabilities.TriggerExecutable.RegisterTrigger:input_type -> capabilities.TriggerRegistrationRequest - 5, // 27: capabilities.TriggerExecutable.UnregisterTrigger:input_type -> capabilities.TriggerRegistrationRequest - 9, // 28: capabilities.TriggerExecutable.AckEvent:input_type -> capabilities.AckEventRequest - 13, // 29: capabilities.Executable.RegisterToWorkflow:input_type -> capabilities.RegisterToWorkflowRequest - 14, // 30: capabilities.Executable.UnregisterFromWorkflow:input_type -> capabilities.UnregisterFromWorkflowRequest - 4, // 31: capabilities.Executable.Execute:input_type -> capabilities.CapabilityRequest - 15, // 32: capabilities.StandardCapabilities.Initialise:input_type -> capabilities.InitialiseRequest - 21, // 33: capabilities.StandardCapabilities.Infos:input_type -> google.protobuf.Empty - 21, // 34: capabilities.Settings.Subscribe:input_type -> google.protobuf.Empty - 1, // 35: capabilities.BaseCapability.Info:output_type -> capabilities.CapabilityInfoReply - 8, // 36: capabilities.TriggerExecutable.RegisterTrigger:output_type -> capabilities.TriggerResponseMessage - 21, // 37: capabilities.TriggerExecutable.UnregisterTrigger:output_type -> google.protobuf.Empty - 21, // 38: capabilities.TriggerExecutable.AckEvent:output_type -> google.protobuf.Empty - 21, // 39: capabilities.Executable.RegisterToWorkflow:output_type -> google.protobuf.Empty - 21, // 40: capabilities.Executable.UnregisterFromWorkflow:output_type -> google.protobuf.Empty - 10, // 41: capabilities.Executable.Execute:output_type -> capabilities.CapabilityResponse - 21, // 42: capabilities.StandardCapabilities.Initialise:output_type -> google.protobuf.Empty - 16, // 43: capabilities.StandardCapabilities.Infos:output_type -> capabilities.CapabilityInfosReply - 17, // 44: capabilities.Settings.Subscribe:output_type -> capabilities.SettingsUpdate - 35, // [35:45] is the sub-list for method output_type - 25, // [25:35] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 22, // 18: capabilities.CapabilityResponse.payload:type_name -> google.protobuf.Any + 12, // 19: capabilities.CapabilityResponse.ocr_attestation:type_name -> capabilities.OCRAttestation + 24, // 20: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail + 13, // 21: capabilities.OCRAttestation.signatures:type_name -> capabilities.AttributedSignature + 14, // 22: capabilities.RegisterToWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata + 21, // 23: capabilities.RegisterToWorkflowRequest.config:type_name -> values.v1.Map + 14, // 24: capabilities.UnregisterFromWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata + 21, // 25: capabilities.UnregisterFromWorkflowRequest.config:type_name -> values.v1.Map + 1, // 26: capabilities.CapabilityInfosReply.infos:type_name -> capabilities.CapabilityInfoReply + 23, // 27: capabilities.BaseCapability.Info:input_type -> google.protobuf.Empty + 5, // 28: capabilities.TriggerExecutable.RegisterTrigger:input_type -> capabilities.TriggerRegistrationRequest + 5, // 29: capabilities.TriggerExecutable.UnregisterTrigger:input_type -> capabilities.TriggerRegistrationRequest + 9, // 30: capabilities.TriggerExecutable.AckEvent:input_type -> capabilities.AckEventRequest + 15, // 31: capabilities.Executable.RegisterToWorkflow:input_type -> capabilities.RegisterToWorkflowRequest + 16, // 32: capabilities.Executable.UnregisterFromWorkflow:input_type -> capabilities.UnregisterFromWorkflowRequest + 4, // 33: capabilities.Executable.Execute:input_type -> capabilities.CapabilityRequest + 17, // 34: capabilities.StandardCapabilities.Initialise:input_type -> capabilities.InitialiseRequest + 23, // 35: capabilities.StandardCapabilities.Infos:input_type -> google.protobuf.Empty + 23, // 36: capabilities.Settings.Subscribe:input_type -> google.protobuf.Empty + 1, // 37: capabilities.BaseCapability.Info:output_type -> capabilities.CapabilityInfoReply + 8, // 38: capabilities.TriggerExecutable.RegisterTrigger:output_type -> capabilities.TriggerResponseMessage + 23, // 39: capabilities.TriggerExecutable.UnregisterTrigger:output_type -> google.protobuf.Empty + 23, // 40: capabilities.TriggerExecutable.AckEvent:output_type -> google.protobuf.Empty + 23, // 41: capabilities.Executable.RegisterToWorkflow:output_type -> google.protobuf.Empty + 23, // 42: capabilities.Executable.UnregisterFromWorkflow:output_type -> google.protobuf.Empty + 10, // 43: capabilities.Executable.Execute:output_type -> capabilities.CapabilityResponse + 23, // 44: capabilities.StandardCapabilities.Initialise:output_type -> google.protobuf.Empty + 18, // 45: capabilities.StandardCapabilities.Infos:output_type -> capabilities.CapabilityInfosReply + 19, // 46: capabilities.Settings.Subscribe:output_type -> capabilities.SettingsUpdate + 37, // [37:47] is the sub-list for method output_type + 27, // [27:37] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_capabilities_proto_init() } @@ -1573,13 +1708,14 @@ func file_capabilities_proto_init() { (*TriggerResponseMessage_Ack)(nil), (*TriggerResponseMessage_Response)(nil), } + file_capabilities_proto_msgTypes[9].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_capabilities_proto_rawDesc), len(file_capabilities_proto_rawDesc)), NumEnums: 1, - NumMessages: 17, + NumMessages: 19, NumExtensions: 0, NumServices: 5, }, diff --git a/pkg/capabilities/pb/capabilities.proto b/pkg/capabilities/pb/capabilities.proto index 8030e64811..e6ff262a7d 100644 --- a/pkg/capabilities/pb/capabilities.proto +++ b/pkg/capabilities/pb/capabilities.proto @@ -127,6 +127,7 @@ message CapabilityResponse { ResponseMetadata metadata = 3; // Used for no DAG SDK google.protobuf.Any payload = 4; + optional OCRAttestation ocr_attestation = 5; } message ResponseMetadata { @@ -144,6 +145,17 @@ message ResponseMetadata { uint32 capdon_n = 2; } +message OCRAttestation { + bytes config_digest = 1; + uint64 sequence_number = 2; + repeated AttributedSignature signatures = 3; +} + +message AttributedSignature { + bytes signature = 1; + uint32 signer = 2; +} + message RegistrationMetadata { string workflow_id = 1; string reference_id = 2; diff --git a/pkg/capabilities/pb/capabilities_helpers.go b/pkg/capabilities/pb/capabilities_helpers.go index 68fb9f04a0..644543a229 100644 --- a/pkg/capabilities/pb/capabilities_helpers.go +++ b/pkg/capabilities/pb/capabilities_helpers.go @@ -73,7 +73,7 @@ func CapabilityRequestToProto(req capabilities.CapabilityRequest) *CapabilityReq DecodedWorkflowName: req.Metadata.DecodedWorkflowName, SpendLimits: spendLimitsToProto(req.Metadata.SpendLimits), WorkflowTag: req.Metadata.WorkflowTag, - ExecutionTimestamp: timeToProto(req.Metadata.ExecutionTimestamp), + ExecutionTimestamp: timeToProto(req.Metadata.ExecutionTimestamp), }, Inputs: values.ProtoMap(inputs), Config: values.ProtoMap(config), @@ -94,13 +94,31 @@ func CapabilityResponseToProto(resp capabilities.CapabilityResponse) *Capability } } + var attestation *OCRAttestation + if resp.OCRAttestation != nil { + respAtt := resp.OCRAttestation + attestation = &OCRAttestation{ + ConfigDigest: respAtt.ConfigDigest[:], + SequenceNumber: respAtt.SequenceNumber, + } + + attestation.Signatures = make([]*AttributedSignature, len(respAtt.Sigs)) + for idx, sig := range respAtt.Sigs { + attestation.Signatures[idx] = &AttributedSignature{ + Signer: sig.Signer, + Signature: sig.Signature, + } + } + } + return &CapabilityResponse{ Value: values.ProtoMap(resp.Value), Metadata: &ResponseMetadata{ Metering: metering, CapdonN: resp.Metadata.CapDON_N, }, - Payload: resp.Payload, + Payload: resp.Payload, + OcrAttestation: attestation, } } @@ -136,7 +154,7 @@ func CapabilityRequestFromProto(pr *CapabilityRequest) (capabilities.CapabilityR DecodedWorkflowName: md.DecodedWorkflowName, SpendLimits: spendLimitsFromProto(md.SpendLimits), WorkflowTag: md.WorkflowTag, - ExecutionTimestamp: timeFromProto(md.ExecutionTimestamp), + ExecutionTimestamp: timeFromProto(md.ExecutionTimestamp), }, Config: config, Inputs: inputs, @@ -159,7 +177,6 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit } var metering []capabilities.MeteringNodeDetail - if pr.Metadata != nil { metering = make([]capabilities.MeteringNodeDetail, len(pr.Metadata.Metering)) @@ -172,13 +189,34 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit } } + var attestation *capabilities.OCRAttestation + if pr.OcrAttestation != nil { + if len(pr.OcrAttestation.ConfigDigest) != 32 { + return capabilities.CapabilityResponse{}, fmt.Errorf("invalid config digest length: expected 32 bytes, got %d", len(pr.OcrAttestation.ConfigDigest)) + } + + attestation = &capabilities.OCRAttestation{ + ConfigDigest: [32]byte(pr.OcrAttestation.ConfigDigest), + SequenceNumber: pr.OcrAttestation.SequenceNumber, + Sigs: make([]capabilities.AttributedSignature, len(pr.OcrAttestation.Signatures)), + } + + for idx, sig := range pr.OcrAttestation.Signatures { + attestation.Sigs[idx] = capabilities.AttributedSignature{ + Signer: sig.Signer, + Signature: sig.Signature, + } + } + } + resp := capabilities.CapabilityResponse{ Value: val, Metadata: capabilities.ResponseMetadata{ Metering: metering, CapDON_N: pr.Metadata.GetCapdonN(), }, - Payload: pr.Payload, + Payload: pr.Payload, + OCRAttestation: attestation, } return resp, err @@ -336,7 +374,7 @@ func TriggerRegistrationRequestToProto(req capabilities.TriggerRegistrationReque WorkflowRegistryChainSelector: md.WorkflowRegistryChainSelector, WorkflowRegistryAddress: md.WorkflowRegistryAddress, EngineVersion: md.EngineVersion, - ExecutionTimestamp: timeToProto(md.ExecutionTimestamp), + ExecutionTimestamp: timeToProto(md.ExecutionTimestamp), }, Config: values.ProtoMap(config), Payload: req.Payload, @@ -401,7 +439,7 @@ func TriggerRegistrationRequestFromProto(req *TriggerRegistrationRequest) (capab WorkflowRegistryChainSelector: md.WorkflowRegistryChainSelector, WorkflowRegistryAddress: md.WorkflowRegistryAddress, EngineVersion: md.EngineVersion, - ExecutionTimestamp: timeFromProto(md.ExecutionTimestamp), + ExecutionTimestamp: timeFromProto(md.ExecutionTimestamp), }, Config: config, Payload: req.Payload, diff --git a/pkg/capabilities/pb/capabilities_helpers_test.go b/pkg/capabilities/pb/capabilities_helpers_test.go index fabe1b8ead..0bf8e78fc2 100644 --- a/pkg/capabilities/pb/capabilities_helpers_test.go +++ b/pkg/capabilities/pb/capabilities_helpers_test.go @@ -1,11 +1,15 @@ package pb_test import ( + "crypto/rand" "testing" "time" + "github.com/google/go-cmp/cmp" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" @@ -82,6 +86,51 @@ func TestCapabilityResponseFromProto(t *testing.T) { resp, err := pb.CapabilityResponseFromProto(&pr) require.NoError(t, err) assert.Equal(t, capabilities.CapabilityResponse{Value: values.EmptyMap()}, resp) + + t.Run("invalid config digest length", func(t *testing.T) { + pr := &pb.CapabilityResponse{ + Value: values.ProtoMap(values.EmptyMap()), + Metadata: &pb.ResponseMetadata{ + CapdonN: 1, + }, + OcrAttestation: &pb.OCRAttestation{ + ConfigDigest: []byte("too-short"), + SequenceNumber: 0, + }, + } + _, err := pb.CapabilityResponseFromProto(pr) + require.ErrorContains(t, err, "invalid config digest length") + }) + t.Run("Round-trip", func(t *testing.T) { + configDigest := ocrtypes.ConfigDigest{} + _, err := rand.Read(configDigest[:]) + require.NoError(t, err) + original := capabilities.CapabilityResponse{ + Value: values.EmptyMap(), + Metadata: capabilities.ResponseMetadata{ + Metering: []capabilities.MeteringNodeDetail{ + { + Peer2PeerID: "peer_id", + SpendUnit: "spend_unit", + SpendValue: "spend_value", + }, + }, + }, + OCRAttestation: &capabilities.OCRAttestation{ + ConfigDigest: configDigest, + SequenceNumber: 12345, + Sigs: []capabilities.AttributedSignature{ + {Signer: 0, Signature: []byte("sig0bytes")}, + {Signer: 1, Signature: []byte("sig1bytes")}, + {Signer: 99, Signature: []byte{}}, + }, + }, + } + protoResp := pb.CapabilityResponseToProto(original) + roundTripped, err := pb.CapabilityResponseFromProto(protoResp) + require.NoError(t, err) + require.Empty(t, cmp.Diff(original, roundTripped, protocmp.Transform()), "Expected capability response to be identical after round trip") + }) } func TestMarshalUnmarshalRequest(t *testing.T) { @@ -100,7 +149,7 @@ func TestMarshalUnmarshalRequest(t *testing.T) { {SpendType: "GAS_12345", Limit: "1000000"}, }, WorkflowTag: "test-workflow-tag", - ExecutionTimestamp: time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), + ExecutionTimestamp: time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), }, Config: &values.Map{Underlying: map[string]values.Value{ testConfigKey: &values.String{Underlying: testConfigValue}, @@ -196,7 +245,7 @@ func TestMarshalUnmarshalTriggerRegistrationRequest(t *testing.T) { {SpendType: "GAS", Limit: "5000"}, }, WorkflowTag: "workflow-tag", - ExecutionTimestamp: time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), + ExecutionTimestamp: time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC), }, Config: &values.Map{Underlying: map[string]values.Value{ testConfigKey: &values.String{Underlying: testConfigValue},