diff --git a/pkg/capabilities/capabilities.go b/pkg/capabilities/capabilities.go index 18903b37de..ea3e0fc6d8 100644 --- a/pkg/capabilities/capabilities.go +++ b/pkg/capabilities/capabilities.go @@ -118,6 +118,9 @@ type RequestMetadata struct { WorkflowRegistryChainSelector string WorkflowRegistryAddress string EngineVersion string + // ExecutionTimestamp is the DonTime-derived execution timestamp. + // Propagated to capability DONs so they can evaluate feature flags atomically with the workflow DON. + ExecutionTimestamp time.Time } func (m *RequestMetadata) ContextWithCRE(ctx context.Context) context.Context { diff --git a/pkg/capabilities/pb/capabilities.pb.go b/pkg/capabilities/pb/capabilities.pb.go index af3460f34f..a1c731f861 100644 --- a/pkg/capabilities/pb/capabilities.pb.go +++ b/pkg/capabilities/pb/capabilities.pb.go @@ -13,6 +13,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -234,6 +235,7 @@ type RequestMetadata struct { WorkflowRegistryChainSelector string `protobuf:"bytes,12,opt,name=workflow_registry_chain_selector,json=workflowRegistryChainSelector,proto3" json:"workflow_registry_chain_selector,omitempty"` WorkflowRegistryAddress string `protobuf:"bytes,13,opt,name=workflow_registry_address,json=workflowRegistryAddress,proto3" json:"workflow_registry_address,omitempty"` EngineVersion string `protobuf:"bytes,14,opt,name=engine_version,json=engineVersion,proto3" json:"engine_version,omitempty"` + ExecutionTimestamp *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=execution_timestamp,json=executionTimestamp,proto3" json:"execution_timestamp,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -359,6 +361,13 @@ func (x *RequestMetadata) GetEngineVersion() string { return "" } +func (x *RequestMetadata) GetExecutionTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.ExecutionTimestamp + } + return nil +} + type CapabilityRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Metadata *RequestMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` @@ -1335,7 +1344,7 @@ var File_capabilities_proto protoreflect.FileDescriptor const file_capabilities_proto_rawDesc = "" + "\n" + - "\x12capabilities.proto\x12\fcapabilities\x1a\x16values/v1/values.proto\x1a metering/pb/meteringdetail.proto\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xe3\x01\n" + + "\x12capabilities.proto\x12\fcapabilities\x1a\x16values/v1/values.proto\x1a metering/pb/meteringdetail.proto\x1a\x19google/protobuf/any.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe3\x01\n" + "\x13CapabilityInfoReply\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12E\n" + "\x0fcapability_type\x18\x02 \x01(\x0e2\x1c.capabilities.CapabilityTypeR\x0ecapabilityType\x12 \n" + @@ -1348,7 +1357,7 @@ const file_capabilities_proto_rawDesc = "" + "SpendLimit\x12\x1d\n" + "\n" + "spend_type\x18\x01 \x01(\tR\tspendType\x12\x14\n" + - "\x05limit\x18\x02 \x01(\tR\x05limit\"\x82\x05\n" + + "\x05limit\x18\x02 \x01(\tR\x05limit\"\xcf\x05\n" + "\x0fRequestMetadata\x12\x1f\n" + "\vworkflow_id\x18\x01 \x01(\tR\n" + "workflowId\x122\n" + @@ -1364,7 +1373,8 @@ const file_capabilities_proto_rawDesc = "" + "\fworkflow_tag\x18\v \x01(\tR\vworkflowTag\x12G\n" + " workflow_registry_chain_selector\x18\f \x01(\tR\x1dworkflowRegistryChainSelector\x12:\n" + "\x19workflow_registry_address\x18\r \x01(\tR\x17workflowRegistryAddress\x12%\n" + - "\x0eengine_version\x18\x0e \x01(\tR\rengineVersionJ\x04\b\x05\x10\x06\"\xc6\x02\n" + + "\x0eengine_version\x18\x0e \x01(\tR\rengineVersion\x12K\n" + + "\x13execution_timestamp\x18\x0f \x01(\v2\x1a.google.protobuf.TimestampR\x12executionTimestampJ\x04\b\x05\x10\x06\"\xc6\x02\n" + "\x11CapabilityRequest\x129\n" + "\bmetadata\x18\x01 \x01(\v2\x1d.capabilities.RequestMetadataR\bmetadata\x12&\n" + "\x06config\x18\x02 \x01(\v2\x0e.values.v1.MapR\x06config\x12&\n" + @@ -1495,61 +1505,63 @@ var file_capabilities_proto_goTypes = []any{ (*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 + (*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 } 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 - 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 - 6, // 12: capabilities.TriggerResponse.event:type_name -> capabilities.TriggerEvent - 20, // 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 - 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 + 18, // 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 + 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 + 6, // 13: capabilities.TriggerResponse.event:type_name -> capabilities.TriggerEvent + 21, // 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 + 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 } func init() { file_capabilities_proto_init() } diff --git a/pkg/capabilities/pb/capabilities.proto b/pkg/capabilities/pb/capabilities.proto index ee55b6764c..8030e64811 100644 --- a/pkg/capabilities/pb/capabilities.proto +++ b/pkg/capabilities/pb/capabilities.proto @@ -8,6 +8,7 @@ import "values/v1/values.proto"; import "metering/pb/meteringdetail.proto"; import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; service BaseCapability { rpc Info (google.protobuf.Empty) returns (CapabilityInfoReply) {} @@ -51,6 +52,7 @@ message RequestMetadata { string workflow_registry_chain_selector = 12; string workflow_registry_address = 13; string engine_version = 14; + google.protobuf.Timestamp execution_timestamp = 15; } message CapabilityRequest { diff --git a/pkg/capabilities/pb/capabilities_helpers.go b/pkg/capabilities/pb/capabilities_helpers.go index 51f885e7ec..68fb9f04a0 100644 --- a/pkg/capabilities/pb/capabilities_helpers.go +++ b/pkg/capabilities/pb/capabilities_helpers.go @@ -3,8 +3,10 @@ package pb import ( "errors" "fmt" + "time" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" meter "github.com/smartcontractkit/chainlink-common/pkg/metering/pb" @@ -71,6 +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), }, Inputs: values.ProtoMap(inputs), Config: values.ProtoMap(config), @@ -133,6 +136,7 @@ func CapabilityRequestFromProto(pr *CapabilityRequest) (capabilities.CapabilityR DecodedWorkflowName: md.DecodedWorkflowName, SpendLimits: spendLimitsFromProto(md.SpendLimits), WorkflowTag: md.WorkflowTag, + ExecutionTimestamp: timeFromProto(md.ExecutionTimestamp), }, Config: config, Inputs: inputs, @@ -332,6 +336,7 @@ func TriggerRegistrationRequestToProto(req capabilities.TriggerRegistrationReque WorkflowRegistryChainSelector: md.WorkflowRegistryChainSelector, WorkflowRegistryAddress: md.WorkflowRegistryAddress, EngineVersion: md.EngineVersion, + ExecutionTimestamp: timeToProto(md.ExecutionTimestamp), }, Config: values.ProtoMap(config), Payload: req.Payload, @@ -339,6 +344,20 @@ func TriggerRegistrationRequestToProto(req capabilities.TriggerRegistrationReque } } +func timeToProto(t time.Time) *timestamppb.Timestamp { + if t.IsZero() { + return nil + } + return timestamppb.New(t) +} + +func timeFromProto(ts *timestamppb.Timestamp) time.Time { + if ts == nil { + return time.Time{} + } + return ts.AsTime() +} + func spendLimitsToProto(limits []capabilities.SpendLimit) []*SpendLimit { result := make([]*SpendLimit, len(limits)) for i, limit := range limits { @@ -382,6 +401,7 @@ func TriggerRegistrationRequestFromProto(req *TriggerRegistrationRequest) (capab WorkflowRegistryChainSelector: md.WorkflowRegistryChainSelector, WorkflowRegistryAddress: md.WorkflowRegistryAddress, EngineVersion: md.EngineVersion, + 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 a10163531a..fabe1b8ead 100644 --- a/pkg/capabilities/pb/capabilities_helpers_test.go +++ b/pkg/capabilities/pb/capabilities_helpers_test.go @@ -2,6 +2,7 @@ package pb_test import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -98,7 +99,8 @@ func TestMarshalUnmarshalRequest(t *testing.T) { {SpendType: "COMPUTE", Limit: "1000"}, {SpendType: "GAS_12345", Limit: "1000000"}, }, - WorkflowTag: "test-workflow-tag", + WorkflowTag: "test-workflow-tag", + 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}, @@ -193,7 +195,8 @@ func TestMarshalUnmarshalTriggerRegistrationRequest(t *testing.T) { SpendLimits: []capabilities.SpendLimit{ {SpendType: "GAS", Limit: "5000"}, }, - WorkflowTag: "workflow-tag", + WorkflowTag: "workflow-tag", + 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},