From 41d370c175739aef73ba4a897ab740ae795e232c Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Wed, 18 Mar 2026 16:26:24 +0100 Subject: [PATCH 1/3] Allow capability DONs to include OCR attestation of the responses --- keystore/corekeys/ocr2key/evm_keyring.go | 18 +- pkg/capabilities/capabilities.go | 54 +++- pkg/capabilities/pb/capabilities.pb.go | 290 +++++++++++++----- pkg/capabilities/pb/capabilities.proto | 12 + pkg/capabilities/pb/capabilities_helpers.go | 48 ++- .../pb/capabilities_helpers_test.go | 49 +++ 6 files changed, 380 insertions(+), 91 deletions(-) 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 18903b37de..6ae3752620 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 = &errResponsePayloadNotAvailable{} + +type errResponsePayloadNotAvailable struct{} + +const errResponsePayloadNotAvailableMsg = "__response_payload_not_available" + +func (e errResponsePayloadNotAvailable) Error() string { + return errResponsePayloadNotAvailableMsg +} + +func (e errResponsePayloadNotAvailable) Is(err error) bool { + return strings.Contains(err.Error(), errResponsePayloadNotAvailableMsg) +} + // CapabilityType enum values. const ( CapabilityTypeUnknown CapabilityType = "unknown" @@ -80,8 +97,41 @@ type CapabilityResponse struct { } type ResponseMetadata struct { - Metering []MeteringNodeDetail - CapDON_N uint32 + Metering []MeteringNodeDetail + CapDON_N uint32 + OCRAttestation *ResponseOCRAttestation +} + +type ResponseOCRAttestation struct { + ConfigDigest ocrtypes.ConfigDigest + SequenceNumber uint64 + Sigs []AttributedSignature +} + +type AttributedSignature struct { + Signature []byte + Signer uint32 +} + +func ResponseToReportData(workflowExecutionID, referenceID string, responsePayload []byte, spendUnit, spendValue string) [32]byte { + 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(spendUnit)) + writeField([]byte(spendValue)) + + var result [32]byte + copy(result[:], hash.Sum(nil)) + return result } type MeteringNodeDetail struct { diff --git a/pkg/capabilities/pb/capabilities.pb.go b/pkg/capabilities/pb/capabilities.pb.go index af3460f34f..6a72ff08f4 100644 --- a/pkg/capabilities/pb/capabilities.pb.go +++ b/pkg/capabilities/pb/capabilities.pb.go @@ -882,9 +882,10 @@ type ResponseMetadata struct { // more than one metering report per node. Metering []*pb1.MeteringReportNodeDetail `protobuf:"bytes,1,rep,name=metering,proto3" json:"metering,omitempty"` // capdon_n represents the total number of nodes in a capability don. - CapdonN uint32 `protobuf:"varint,2,opt,name=capdon_n,json=capdonN,proto3" json:"capdon_n,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + CapdonN uint32 `protobuf:"varint,2,opt,name=capdon_n,json=capdonN,proto3" json:"capdon_n,omitempty"` + OcrAttestation *ResponseOCRAttestation `protobuf:"bytes,3,opt,name=ocr_attestation,json=ocrAttestation,proto3,oneof" json:"ocr_attestation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ResponseMetadata) Reset() { @@ -931,6 +932,125 @@ func (x *ResponseMetadata) GetCapdonN() uint32 { return 0 } +func (x *ResponseMetadata) GetOcrAttestation() *ResponseOCRAttestation { + if x != nil { + return x.OcrAttestation + } + return nil +} + +type ResponseOCRAttestation 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 *ResponseOCRAttestation) Reset() { + *x = ResponseOCRAttestation{} + mi := &file_capabilities_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResponseOCRAttestation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResponseOCRAttestation) ProtoMessage() {} + +func (x *ResponseOCRAttestation) 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 ResponseOCRAttestation.ProtoReflect.Descriptor instead. +func (*ResponseOCRAttestation) Descriptor() ([]byte, []int) { + return file_capabilities_proto_rawDescGZIP(), []int{11} +} + +func (x *ResponseOCRAttestation) GetConfigDigest() []byte { + if x != nil { + return x.ConfigDigest + } + return nil +} + +func (x *ResponseOCRAttestation) GetSequenceNumber() uint64 { + if x != nil { + return x.SequenceNumber + } + return 0 +} + +func (x *ResponseOCRAttestation) 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"` @@ -942,7 +1062,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) } @@ -954,7 +1074,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 { @@ -967,7 +1087,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 { @@ -1001,7 +1121,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) } @@ -1013,7 +1133,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 { @@ -1026,7 +1146,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 { @@ -1053,7 +1173,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) } @@ -1065,7 +1185,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 { @@ -1078,7 +1198,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 { @@ -1116,7 +1236,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) } @@ -1128,7 +1248,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 { @@ -1141,7 +1261,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 { @@ -1244,7 +1364,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) } @@ -1256,7 +1376,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 { @@ -1269,7 +1389,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 { @@ -1289,7 +1409,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) } @@ -1301,7 +1421,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 { @@ -1314,7 +1434,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 { @@ -1400,10 +1520,21 @@ const file_capabilities_proto_rawDesc = "" + "\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\"\xd5\x01\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\x12R\n" + + "\x0focr_attestation\x18\x03 \x01(\v2$.capabilities.ResponseOCRAttestationH\x00R\x0eocrAttestation\x88\x01\x01B\x12\n" + + "\x10_ocr_attestation\"\xa9\x01\n" + + "\x16ResponseOCRAttestation\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" + @@ -1475,7 +1606,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 @@ -1489,67 +1620,71 @@ 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 - (*pb.Map)(nil), // 18: values.v1.Map - (*anypb.Any)(nil), // 19: google.protobuf.Any - (*emptypb.Empty)(nil), // 20: google.protobuf.Empty - (*pb1.MeteringReportNodeDetail)(nil), // 21: metering.MeteringReportNodeDetail + (*ResponseOCRAttestation)(nil), // 12: capabilities.ResponseOCRAttestation + (*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 + (*pb.Map)(nil), // 20: values.v1.Map + (*anypb.Any)(nil), // 21: google.protobuf.Any + (*emptypb.Empty)(nil), // 22: google.protobuf.Empty + (*pb1.MeteringReportNodeDetail)(nil), // 23: 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 3, // 2: capabilities.CapabilityRequest.metadata:type_name -> capabilities.RequestMetadata - 18, // 3: capabilities.CapabilityRequest.config:type_name -> values.v1.Map - 18, // 4: capabilities.CapabilityRequest.inputs:type_name -> values.v1.Map - 19, // 5: capabilities.CapabilityRequest.payload:type_name -> google.protobuf.Any - 19, // 6: capabilities.CapabilityRequest.configPayload:type_name -> google.protobuf.Any + 20, // 3: capabilities.CapabilityRequest.config:type_name -> values.v1.Map + 20, // 4: capabilities.CapabilityRequest.inputs:type_name -> values.v1.Map + 21, // 5: capabilities.CapabilityRequest.payload:type_name -> google.protobuf.Any + 21, // 6: capabilities.CapabilityRequest.configPayload:type_name -> google.protobuf.Any 3, // 7: capabilities.TriggerRegistrationRequest.metadata:type_name -> capabilities.RequestMetadata - 18, // 8: capabilities.TriggerRegistrationRequest.config:type_name -> values.v1.Map - 19, // 9: capabilities.TriggerRegistrationRequest.payload:type_name -> google.protobuf.Any - 18, // 10: capabilities.TriggerEvent.outputs:type_name -> values.v1.Map - 19, // 11: capabilities.TriggerEvent.payload:type_name -> google.protobuf.Any + 20, // 8: capabilities.TriggerRegistrationRequest.config:type_name -> values.v1.Map + 21, // 9: capabilities.TriggerRegistrationRequest.payload:type_name -> google.protobuf.Any + 20, // 10: capabilities.TriggerEvent.outputs:type_name -> values.v1.Map + 21, // 11: capabilities.TriggerEvent.payload:type_name -> google.protobuf.Any 6, // 12: capabilities.TriggerResponse.event:type_name -> capabilities.TriggerEvent - 20, // 13: capabilities.TriggerResponseMessage.ack:type_name -> google.protobuf.Empty + 22, // 13: capabilities.TriggerResponseMessage.ack:type_name -> google.protobuf.Empty 7, // 14: capabilities.TriggerResponseMessage.response:type_name -> capabilities.TriggerResponse - 18, // 15: capabilities.CapabilityResponse.value:type_name -> values.v1.Map + 20, // 15: capabilities.CapabilityResponse.value:type_name -> values.v1.Map 11, // 16: capabilities.CapabilityResponse.metadata:type_name -> capabilities.ResponseMetadata - 19, // 17: capabilities.CapabilityResponse.payload:type_name -> google.protobuf.Any - 21, // 18: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail - 12, // 19: capabilities.RegisterToWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata - 18, // 20: capabilities.RegisterToWorkflowRequest.config:type_name -> values.v1.Map - 12, // 21: capabilities.UnregisterFromWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata - 18, // 22: capabilities.UnregisterFromWorkflowRequest.config:type_name -> values.v1.Map - 1, // 23: capabilities.CapabilityInfosReply.infos:type_name -> capabilities.CapabilityInfoReply - 20, // 24: capabilities.BaseCapability.Info:input_type -> google.protobuf.Empty - 5, // 25: capabilities.TriggerExecutable.RegisterTrigger:input_type -> capabilities.TriggerRegistrationRequest - 5, // 26: capabilities.TriggerExecutable.UnregisterTrigger:input_type -> capabilities.TriggerRegistrationRequest - 9, // 27: capabilities.TriggerExecutable.AckEvent:input_type -> capabilities.AckEventRequest - 13, // 28: capabilities.Executable.RegisterToWorkflow:input_type -> capabilities.RegisterToWorkflowRequest - 14, // 29: capabilities.Executable.UnregisterFromWorkflow:input_type -> capabilities.UnregisterFromWorkflowRequest - 4, // 30: capabilities.Executable.Execute:input_type -> capabilities.CapabilityRequest - 15, // 31: capabilities.StandardCapabilities.Initialise:input_type -> capabilities.InitialiseRequest - 20, // 32: capabilities.StandardCapabilities.Infos:input_type -> google.protobuf.Empty - 20, // 33: capabilities.Settings.Subscribe:input_type -> google.protobuf.Empty - 1, // 34: capabilities.BaseCapability.Info:output_type -> capabilities.CapabilityInfoReply - 8, // 35: capabilities.TriggerExecutable.RegisterTrigger:output_type -> capabilities.TriggerResponseMessage - 20, // 36: capabilities.TriggerExecutable.UnregisterTrigger:output_type -> google.protobuf.Empty - 20, // 37: capabilities.TriggerExecutable.AckEvent:output_type -> google.protobuf.Empty - 20, // 38: capabilities.Executable.RegisterToWorkflow:output_type -> google.protobuf.Empty - 20, // 39: capabilities.Executable.UnregisterFromWorkflow:output_type -> google.protobuf.Empty - 10, // 40: capabilities.Executable.Execute:output_type -> capabilities.CapabilityResponse - 20, // 41: capabilities.StandardCapabilities.Initialise:output_type -> google.protobuf.Empty - 16, // 42: capabilities.StandardCapabilities.Infos:output_type -> capabilities.CapabilityInfosReply - 17, // 43: capabilities.Settings.Subscribe:output_type -> capabilities.SettingsUpdate - 34, // [34:44] is the sub-list for method output_type - 24, // [24:34] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 21, // 17: capabilities.CapabilityResponse.payload:type_name -> google.protobuf.Any + 23, // 18: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail + 12, // 19: capabilities.ResponseMetadata.ocr_attestation:type_name -> capabilities.ResponseOCRAttestation + 13, // 20: capabilities.ResponseOCRAttestation.signatures:type_name -> capabilities.AttributedSignature + 14, // 21: capabilities.RegisterToWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata + 20, // 22: capabilities.RegisterToWorkflowRequest.config:type_name -> values.v1.Map + 14, // 23: capabilities.UnregisterFromWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata + 20, // 24: capabilities.UnregisterFromWorkflowRequest.config:type_name -> values.v1.Map + 1, // 25: capabilities.CapabilityInfosReply.infos:type_name -> capabilities.CapabilityInfoReply + 22, // 26: capabilities.BaseCapability.Info:input_type -> google.protobuf.Empty + 5, // 27: capabilities.TriggerExecutable.RegisterTrigger:input_type -> capabilities.TriggerRegistrationRequest + 5, // 28: capabilities.TriggerExecutable.UnregisterTrigger:input_type -> capabilities.TriggerRegistrationRequest + 9, // 29: capabilities.TriggerExecutable.AckEvent:input_type -> capabilities.AckEventRequest + 15, // 30: capabilities.Executable.RegisterToWorkflow:input_type -> capabilities.RegisterToWorkflowRequest + 16, // 31: capabilities.Executable.UnregisterFromWorkflow:input_type -> capabilities.UnregisterFromWorkflowRequest + 4, // 32: capabilities.Executable.Execute:input_type -> capabilities.CapabilityRequest + 17, // 33: capabilities.StandardCapabilities.Initialise:input_type -> capabilities.InitialiseRequest + 22, // 34: capabilities.StandardCapabilities.Infos:input_type -> google.protobuf.Empty + 22, // 35: capabilities.Settings.Subscribe:input_type -> google.protobuf.Empty + 1, // 36: capabilities.BaseCapability.Info:output_type -> capabilities.CapabilityInfoReply + 8, // 37: capabilities.TriggerExecutable.RegisterTrigger:output_type -> capabilities.TriggerResponseMessage + 22, // 38: capabilities.TriggerExecutable.UnregisterTrigger:output_type -> google.protobuf.Empty + 22, // 39: capabilities.TriggerExecutable.AckEvent:output_type -> google.protobuf.Empty + 22, // 40: capabilities.Executable.RegisterToWorkflow:output_type -> google.protobuf.Empty + 22, // 41: capabilities.Executable.UnregisterFromWorkflow:output_type -> google.protobuf.Empty + 10, // 42: capabilities.Executable.Execute:output_type -> capabilities.CapabilityResponse + 22, // 43: capabilities.StandardCapabilities.Initialise:output_type -> google.protobuf.Empty + 18, // 44: capabilities.StandardCapabilities.Infos:output_type -> capabilities.CapabilityInfosReply + 19, // 45: capabilities.Settings.Subscribe:output_type -> capabilities.SettingsUpdate + 36, // [36:46] is the sub-list for method output_type + 26, // [26:36] is the sub-list for method input_type + 26, // [26:26] is the sub-list for extension type_name + 26, // [26:26] is the sub-list for extension extendee + 0, // [0:26] is the sub-list for field type_name } func init() { file_capabilities_proto_init() } @@ -1561,13 +1696,14 @@ func file_capabilities_proto_init() { (*TriggerResponseMessage_Ack)(nil), (*TriggerResponseMessage_Response)(nil), } + file_capabilities_proto_msgTypes[10].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 ee55b6764c..ebb34a778e 100644 --- a/pkg/capabilities/pb/capabilities.proto +++ b/pkg/capabilities/pb/capabilities.proto @@ -140,6 +140,18 @@ message ResponseMetadata { repeated metering.MeteringReportNodeDetail metering = 1; // capdon_n represents the total number of nodes in a capability don. uint32 capdon_n = 2; + optional ResponseOCRAttestation ocr_attestation = 3; +} + +message ResponseOCRAttestation { + bytes config_digest = 1; + uint64 sequence_number = 2; + repeated AttributedSignature signatures = 3; +} + +message AttributedSignature { + bytes signature = 1; + uint32 signer = 2; } message RegistrationMetadata { diff --git a/pkg/capabilities/pb/capabilities_helpers.go b/pkg/capabilities/pb/capabilities_helpers.go index 51f885e7ec..fdd88aaf33 100644 --- a/pkg/capabilities/pb/capabilities_helpers.go +++ b/pkg/capabilities/pb/capabilities_helpers.go @@ -91,11 +91,29 @@ func CapabilityResponseToProto(resp capabilities.CapabilityResponse) *Capability } } + var attestation *ResponseOCRAttestation + if resp.Metadata.OCRAttestation != nil { + respAtt := resp.Metadata.OCRAttestation + attestation = &ResponseOCRAttestation{ + 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, + Metering: metering, + CapdonN: resp.Metadata.CapDON_N, + OcrAttestation: attestation, }, Payload: resp.Payload, } @@ -155,7 +173,7 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit } var metering []capabilities.MeteringNodeDetail - + var attestation *capabilities.ResponseOCRAttestation if pr.Metadata != nil { metering = make([]capabilities.MeteringNodeDetail, len(pr.Metadata.Metering)) @@ -166,13 +184,33 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit SpendValue: detail.SpendValue, } } + + if pr.Metadata.OcrAttestation != nil { + if len(pr.Metadata.OcrAttestation.ConfigDigest) != 32 { + return capabilities.CapabilityResponse{}, fmt.Errorf("invalid config digest length: expected 32 bytes, got %d", len(pr.Metadata.OcrAttestation.ConfigDigest)) + } + + attestation = &capabilities.ResponseOCRAttestation{ + ConfigDigest: [32]byte(pr.Metadata.OcrAttestation.ConfigDigest), + SequenceNumber: pr.Metadata.OcrAttestation.SequenceNumber, + Sigs: make([]capabilities.AttributedSignature, len(pr.Metadata.OcrAttestation.Signatures)), + } + + for idx, sig := range pr.Metadata.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(), + Metering: metering, + CapDON_N: pr.Metadata.GetCapdonN(), + OCRAttestation: attestation, }, Payload: pr.Payload, } diff --git a/pkg/capabilities/pb/capabilities_helpers_test.go b/pkg/capabilities/pb/capabilities_helpers_test.go index a10163531a..a6912b23df 100644 --- a/pkg/capabilities/pb/capabilities_helpers_test.go +++ b/pkg/capabilities/pb/capabilities_helpers_test.go @@ -1,10 +1,14 @@ package pb_test import ( + "crypto/rand" "testing" + "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" @@ -81,6 +85,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.ResponseOCRAttestation{ + 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.ResponseOCRAttestation{ + 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) { From 56108fe57691a3b9bec848cbbf8c39807053a1c6 Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Thu, 26 Mar 2026 13:17:20 +0100 Subject: [PATCH 2/3] Added tests for ResponseToReportData --- pkg/capabilities/capabilities.go | 25 +++++- pkg/capabilities/capabilities_test.go | 111 ++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go index 6ae3752620..d1a6816021 100644 --- a/pkg/capabilities/capabilities.go +++ b/pkg/capabilities/capabilities.go @@ -113,7 +113,23 @@ type AttributedSignature struct { Signer uint32 } -func ResponseToReportData(workflowExecutionID, referenceID string, responsePayload []byte, spendUnit, spendValue string) [32]byte { +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)) @@ -126,12 +142,12 @@ func ResponseToReportData(workflowExecutionID, referenceID string, responsePaylo writeField([]byte(workflowExecutionID)) writeField([]byte(referenceID)) writeField(responsePayload) - writeField([]byte(spendUnit)) - writeField([]byte(spendValue)) + writeField([]byte(metering.SpendUnit)) + writeField([]byte(metering.SpendValue)) var result [32]byte copy(result[:], hash.Sum(nil)) - return result + return result, nil } type MeteringNodeDetail struct { @@ -168,6 +184,7 @@ type RequestMetadata struct { WorkflowRegistryChainSelector string WorkflowRegistryAddress string EngineVersion string + ExecutionTimestamp int64 } func (m *RequestMetadata) ContextWithCRE(ctx context.Context) context.Context { diff --git a/pkg/capabilities/capabilities_test.go b/pkg/capabilities/capabilities_test.go index 06e3bac43b..7b3f85983d 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, "metadata.Metering must contain exactly one") + + _, err = ResponseToReportData("w", "r", nil, ResponseMetadata{ + Metering: []MeteringNodeDetail{ + {SpendUnit: "a", SpendValue: "1"}, + {SpendUnit: "b", SpendValue: "2"}, + }, + }) + require.ErrorContains(t, err, "metadata.Metering must contain exactly one") + }) +} From 587c83f669b248c84be8cc0760b74abd3c1638aa Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko Date: Fri, 27 Mar 2026 14:45:19 +0100 Subject: [PATCH 3/3] Move OCR attestation to response --- pkg/capabilities/capabilities.go | 10 +-- pkg/capabilities/capabilities_test.go | 4 +- pkg/capabilities/pb/capabilities.pb.go | 72 +++++++++---------- pkg/capabilities/pb/capabilities.proto | 4 +- pkg/capabilities/pb/capabilities_helpers.go | 42 +++++------ .../pb/capabilities_helpers_test.go | 2 +- 6 files changed, 67 insertions(+), 67 deletions(-) diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go index d1a6816021..6214d12520 100644 --- a/pkg/capabilities/capabilities.go +++ b/pkg/capabilities/capabilities.go @@ -93,16 +93,16 @@ type CapabilityResponse struct { Metadata ResponseMetadata // Payload is used for no DAG workflows - Payload *anypb.Any + Payload *anypb.Any + OCRAttestation *OCRAttestation } type ResponseMetadata struct { - Metering []MeteringNodeDetail - CapDON_N uint32 - OCRAttestation *ResponseOCRAttestation + Metering []MeteringNodeDetail + CapDON_N uint32 } -type ResponseOCRAttestation struct { +type OCRAttestation struct { ConfigDigest ocrtypes.ConfigDigest SequenceNumber uint64 Sigs []AttributedSignature diff --git a/pkg/capabilities/capabilities_test.go b/pkg/capabilities/capabilities_test.go index 7b3f85983d..272622b535 100644 --- a/pkg/capabilities/capabilities_test.go +++ b/pkg/capabilities/capabilities_test.go @@ -460,7 +460,7 @@ func TestResponseToReportData(t *testing.T) { t.Run("metering must contain exactly one entry", func(t *testing.T) { _, err := ResponseToReportData("w", "r", nil, ResponseMetadata{}) - require.ErrorContains(t, err, "metadata.Metering must contain exactly one") + 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{ @@ -468,6 +468,6 @@ func TestResponseToReportData(t *testing.T) { {SpendUnit: "b", SpendValue: "2"}, }, }) - require.ErrorContains(t, err, "metadata.Metering must contain exactly one") + 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 6a72ff08f4..3be8aecfcc 100644 --- a/pkg/capabilities/pb/capabilities.pb.go +++ b/pkg/capabilities/pb/capabilities.pb.go @@ -806,9 +806,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() { @@ -869,6 +870,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 @@ -882,10 +890,9 @@ type ResponseMetadata struct { // more than one metering report per node. Metering []*pb1.MeteringReportNodeDetail `protobuf:"bytes,1,rep,name=metering,proto3" json:"metering,omitempty"` // capdon_n represents the total number of nodes in a capability don. - CapdonN uint32 `protobuf:"varint,2,opt,name=capdon_n,json=capdonN,proto3" json:"capdon_n,omitempty"` - OcrAttestation *ResponseOCRAttestation `protobuf:"bytes,3,opt,name=ocr_attestation,json=ocrAttestation,proto3,oneof" json:"ocr_attestation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + CapdonN uint32 `protobuf:"varint,2,opt,name=capdon_n,json=capdonN,proto3" json:"capdon_n,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ResponseMetadata) Reset() { @@ -932,14 +939,7 @@ func (x *ResponseMetadata) GetCapdonN() uint32 { return 0 } -func (x *ResponseMetadata) GetOcrAttestation() *ResponseOCRAttestation { - if x != nil { - return x.OcrAttestation - } - return nil -} - -type ResponseOCRAttestation struct { +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"` @@ -948,20 +948,20 @@ type ResponseOCRAttestation struct { sizeCache protoimpl.SizeCache } -func (x *ResponseOCRAttestation) Reset() { - *x = ResponseOCRAttestation{} +func (x *OCRAttestation) Reset() { + *x = OCRAttestation{} mi := &file_capabilities_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *ResponseOCRAttestation) String() string { +func (x *OCRAttestation) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ResponseOCRAttestation) ProtoMessage() {} +func (*OCRAttestation) ProtoMessage() {} -func (x *ResponseOCRAttestation) ProtoReflect() protoreflect.Message { +func (x *OCRAttestation) ProtoReflect() protoreflect.Message { mi := &file_capabilities_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -973,26 +973,26 @@ func (x *ResponseOCRAttestation) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ResponseOCRAttestation.ProtoReflect.Descriptor instead. -func (*ResponseOCRAttestation) Descriptor() ([]byte, []int) { +// Deprecated: Use OCRAttestation.ProtoReflect.Descriptor instead. +func (*OCRAttestation) Descriptor() ([]byte, []int) { return file_capabilities_proto_rawDescGZIP(), []int{11} } -func (x *ResponseOCRAttestation) GetConfigDigest() []byte { +func (x *OCRAttestation) GetConfigDigest() []byte { if x != nil { return x.ConfigDigest } return nil } -func (x *ResponseOCRAttestation) GetSequenceNumber() uint64 { +func (x *OCRAttestation) GetSequenceNumber() uint64 { if x != nil { return x.SequenceNumber } return 0 } -func (x *ResponseOCRAttestation) GetSignatures() []*AttributedSignature { +func (x *OCRAttestation) GetSignatures() []*AttributedSignature { if x != nil { return x.Signatures } @@ -1515,18 +1515,18 @@ 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\"\xd5\x01\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\x12R\n" + - "\x0focr_attestation\x18\x03 \x01(\v2$.capabilities.ResponseOCRAttestationH\x00R\x0eocrAttestation\x88\x01\x01B\x12\n" + - "\x10_ocr_attestation\"\xa9\x01\n" + - "\x16ResponseOCRAttestation\x12#\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" + @@ -1620,7 +1620,7 @@ var file_capabilities_proto_goTypes = []any{ (*AckEventRequest)(nil), // 9: capabilities.AckEventRequest (*CapabilityResponse)(nil), // 10: capabilities.CapabilityResponse (*ResponseMetadata)(nil), // 11: capabilities.ResponseMetadata - (*ResponseOCRAttestation)(nil), // 12: capabilities.ResponseOCRAttestation + (*OCRAttestation)(nil), // 12: capabilities.OCRAttestation (*AttributedSignature)(nil), // 13: capabilities.AttributedSignature (*RegistrationMetadata)(nil), // 14: capabilities.RegistrationMetadata (*RegisterToWorkflowRequest)(nil), // 15: capabilities.RegisterToWorkflowRequest @@ -1652,9 +1652,9 @@ var file_capabilities_proto_depIdxs = []int32{ 20, // 15: capabilities.CapabilityResponse.value:type_name -> values.v1.Map 11, // 16: capabilities.CapabilityResponse.metadata:type_name -> capabilities.ResponseMetadata 21, // 17: capabilities.CapabilityResponse.payload:type_name -> google.protobuf.Any - 23, // 18: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail - 12, // 19: capabilities.ResponseMetadata.ocr_attestation:type_name -> capabilities.ResponseOCRAttestation - 13, // 20: capabilities.ResponseOCRAttestation.signatures:type_name -> capabilities.AttributedSignature + 12, // 18: capabilities.CapabilityResponse.ocr_attestation:type_name -> capabilities.OCRAttestation + 23, // 19: capabilities.ResponseMetadata.metering:type_name -> metering.MeteringReportNodeDetail + 13, // 20: capabilities.OCRAttestation.signatures:type_name -> capabilities.AttributedSignature 14, // 21: capabilities.RegisterToWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata 20, // 22: capabilities.RegisterToWorkflowRequest.config:type_name -> values.v1.Map 14, // 23: capabilities.UnregisterFromWorkflowRequest.metadata:type_name -> capabilities.RegistrationMetadata @@ -1696,7 +1696,7 @@ func file_capabilities_proto_init() { (*TriggerResponseMessage_Ack)(nil), (*TriggerResponseMessage_Response)(nil), } - file_capabilities_proto_msgTypes[10].OneofWrappers = []any{} + file_capabilities_proto_msgTypes[9].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/pkg/capabilities/pb/capabilities.proto b/pkg/capabilities/pb/capabilities.proto index ebb34a778e..37be2e5a86 100644 --- a/pkg/capabilities/pb/capabilities.proto +++ b/pkg/capabilities/pb/capabilities.proto @@ -125,6 +125,7 @@ message CapabilityResponse { ResponseMetadata metadata = 3; // Used for no DAG SDK google.protobuf.Any payload = 4; + optional OCRAttestation ocr_attestation = 5; } message ResponseMetadata { @@ -140,10 +141,9 @@ message ResponseMetadata { repeated metering.MeteringReportNodeDetail metering = 1; // capdon_n represents the total number of nodes in a capability don. uint32 capdon_n = 2; - optional ResponseOCRAttestation ocr_attestation = 3; } -message ResponseOCRAttestation { +message OCRAttestation { bytes config_digest = 1; uint64 sequence_number = 2; repeated AttributedSignature signatures = 3; diff --git a/pkg/capabilities/pb/capabilities_helpers.go b/pkg/capabilities/pb/capabilities_helpers.go index fdd88aaf33..be023d2cdd 100644 --- a/pkg/capabilities/pb/capabilities_helpers.go +++ b/pkg/capabilities/pb/capabilities_helpers.go @@ -91,10 +91,10 @@ func CapabilityResponseToProto(resp capabilities.CapabilityResponse) *Capability } } - var attestation *ResponseOCRAttestation - if resp.Metadata.OCRAttestation != nil { - respAtt := resp.Metadata.OCRAttestation - attestation = &ResponseOCRAttestation{ + var attestation *OCRAttestation + if resp.OCRAttestation != nil { + respAtt := resp.OCRAttestation + attestation = &OCRAttestation{ ConfigDigest: respAtt.ConfigDigest[:], SequenceNumber: respAtt.SequenceNumber, } @@ -111,11 +111,11 @@ func CapabilityResponseToProto(resp capabilities.CapabilityResponse) *Capability return &CapabilityResponse{ Value: values.ProtoMap(resp.Value), Metadata: &ResponseMetadata{ - Metering: metering, - CapdonN: resp.Metadata.CapDON_N, - OcrAttestation: attestation, + Metering: metering, + CapdonN: resp.Metadata.CapDON_N, }, - Payload: resp.Payload, + Payload: resp.Payload, + OcrAttestation: attestation, } } @@ -173,7 +173,7 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit } var metering []capabilities.MeteringNodeDetail - var attestation *capabilities.ResponseOCRAttestation + var attestation *capabilities.OCRAttestation if pr.Metadata != nil { metering = make([]capabilities.MeteringNodeDetail, len(pr.Metadata.Metering)) @@ -185,18 +185,18 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit } } - if pr.Metadata.OcrAttestation != nil { - if len(pr.Metadata.OcrAttestation.ConfigDigest) != 32 { - return capabilities.CapabilityResponse{}, fmt.Errorf("invalid config digest length: expected 32 bytes, got %d", len(pr.Metadata.OcrAttestation.ConfigDigest)) + 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.ResponseOCRAttestation{ - ConfigDigest: [32]byte(pr.Metadata.OcrAttestation.ConfigDigest), - SequenceNumber: pr.Metadata.OcrAttestation.SequenceNumber, - Sigs: make([]capabilities.AttributedSignature, len(pr.Metadata.OcrAttestation.Signatures)), + 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.Metadata.OcrAttestation.Signatures { + for idx, sig := range pr.OcrAttestation.Signatures { attestation.Sigs[idx] = capabilities.AttributedSignature{ Signer: sig.Signer, Signature: sig.Signature, @@ -208,11 +208,11 @@ func CapabilityResponseFromProto(pr *CapabilityResponse) (capabilities.Capabilit resp := capabilities.CapabilityResponse{ Value: val, Metadata: capabilities.ResponseMetadata{ - Metering: metering, - CapDON_N: pr.Metadata.GetCapdonN(), - OCRAttestation: attestation, + Metering: metering, + CapDON_N: pr.Metadata.GetCapdonN(), }, - Payload: pr.Payload, + Payload: pr.Payload, + OCRAttestation: attestation, } return resp, err diff --git a/pkg/capabilities/pb/capabilities_helpers_test.go b/pkg/capabilities/pb/capabilities_helpers_test.go index a6912b23df..207703211e 100644 --- a/pkg/capabilities/pb/capabilities_helpers_test.go +++ b/pkg/capabilities/pb/capabilities_helpers_test.go @@ -114,7 +114,7 @@ func TestCapabilityResponseFromProto(t *testing.T) { SpendValue: "spend_value", }, }, - OCRAttestation: &capabilities.ResponseOCRAttestation{ + OCRAttestation: &capabilities.OCRAttestation{ ConfigDigest: configDigest, SequenceNumber: 12345, Sigs: []capabilities.AttributedSignature{