From 4740d2975c1a8d0a7b05d5e0e6ec946df1704d52 Mon Sep 17 00:00:00 2001 From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com> Date: Sat, 28 Feb 2026 10:50:57 -0800 Subject: [PATCH 01/12] feat: add --limits flag to cre workflow simulate # Conflicts: # cmd/root.go # cmd/workflow/simulate/simulate.go # cmd/workflow/workflow.go --- cmd/root.go | 20 +++++++---- cmd/workflow/simulate/capabilities.go | 29 ++++++++++++--- cmd/workflow/simulate/simulate.go | 52 +++++++++++++++++++++++++-- cmd/workflow/workflow.go | 2 ++ 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index fe2cf42a..11bfd37a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -435,8 +435,10 @@ func isLoadSettings(cmd *cobra.Command) bool { "cre completion zsh": {}, "cre help": {}, "cre update": {}, - "cre workflow": {}, - "cre workflow custom-build": {}, + "cre workflow": {}, + "cre workflow custom-build": {}, + "cre workflow limits": {}, + "cre workflow limits export": {}, "cre workflow build": {}, "cre account": {}, "cre secrets": {}, @@ -464,11 +466,13 @@ func isLoadCredentials(cmd *cobra.Command) bool { "cre help": {}, "cre generate-bindings": {}, "cre update": {}, - "cre workflow": {}, + "cre workflow": {}, + "cre workflow limits": {}, + "cre workflow limits export": {}, + "cre account": {}, + "cre secrets": {}, "cre workflow build": {}, "cre workflow hash": {}, - "cre account": {}, - "cre secrets": {}, "cre templates": {}, "cre templates list": {}, "cre templates add": {}, @@ -535,10 +539,12 @@ func shouldShowSpinner(cmd *cobra.Command) bool { "cre login": {}, // Has its own interactive flow "cre logout": {}, "cre update": {}, - "cre workflow": {}, // Just shows help + "cre workflow": {}, // Just shows help + "cre workflow limits": {}, // Just shows help + "cre workflow limits export": {}, // Static data, no project needed + "cre account": {}, // Just shows help "cre workflow build": {}, // Offline command, no async init "cre workflow hash": {}, // Offline command, has own spinner - "cre account": {}, // Just shows help "cre secrets": {}, // Just shows help "cre templates": {}, // Just shows help "cre templates list": {}, diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index 69f7273c..57cc2b5b 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -39,6 +39,7 @@ func NewManualTriggerCapabilities( registry *capabilities.Registry, cfg ManualTriggerCapabilitiesConfig, dryRunChainWrite bool, + limits *SimulationLimits, ) (*ManualTriggers, error) { // Cron manualCronTrigger := fakes.NewManualCronTriggerService(lggr) @@ -72,7 +73,13 @@ func NewManualTriggerCapabilities( dryRunChainWrite, ) - evmServer := evmserver.NewClientServer(evm) + // Wrap with limits enforcement if limits are enabled + var evmCap evmserver.ClientCapability = evm + if limits != nil { + evmCap = NewLimitedEVMChain(evm, limits) + } + + evmServer := evmserver.NewClientServer(evmCap) if err := registry.Add(ctx, evmServer); err != nil { return nil, err } @@ -129,7 +136,7 @@ func (m *ManualTriggers) Close() error { } // NewFakeCapabilities builds faked capabilities, then registers them with the capability registry. -func NewFakeActionCapabilities(ctx context.Context, lggr logger.Logger, registry *capabilities.Registry, secretsPath string) ([]services.Service, error) { +func NewFakeActionCapabilities(ctx context.Context, lggr logger.Logger, registry *capabilities.Registry, secretsPath string, limits *SimulationLimits) ([]services.Service, error) { caps := make([]services.Service, 0) // Consensus @@ -142,7 +149,11 @@ func NewFakeActionCapabilities(ctx context.Context, lggr logger.Logger, registry signers = append(signers, signer) } fakeConsensusNoDAG := fakes.NewFakeConsensusNoDAG(signers, lggr) - fakeConsensusServer := consensusserver.NewConsensusServer(fakeConsensusNoDAG) + var consensusCap consensusserver.ConsensusCapability = fakeConsensusNoDAG + if limits != nil { + consensusCap = NewLimitedConsensusNoDAG(fakeConsensusNoDAG, limits) + } + fakeConsensusServer := consensusserver.NewConsensusServer(consensusCap) if err := registry.Add(ctx, fakeConsensusServer); err != nil { return nil, err } @@ -150,7 +161,11 @@ func NewFakeActionCapabilities(ctx context.Context, lggr logger.Logger, registry // HTTP Action httpAction := fakes.NewDirectHTTPAction(lggr) - httpActionServer := httpserver.NewClientServer(httpAction) + var httpCap httpserver.ClientCapability = httpAction + if limits != nil { + httpCap = NewLimitedHTTPAction(httpAction, limits) + } + httpActionServer := httpserver.NewClientServer(httpCap) if err := registry.Add(ctx, httpActionServer); err != nil { return nil, err } @@ -158,7 +173,11 @@ func NewFakeActionCapabilities(ctx context.Context, lggr logger.Logger, registry // Conf HTTP Action confHTTPAction := fakes.NewDirectConfidentialHTTPAction(lggr, secretsPath) - confHTTPActionServer := confhttpserver.NewClientServer(confHTTPAction) + var confHTTPCap confhttpserver.ClientCapability = confHTTPAction + if limits != nil { + confHTTPCap = NewLimitedConfidentialHTTPAction(confHTTPAction, limits) + } + confHTTPActionServer := confhttpserver.NewClientServer(confHTTPCap) if err := registry.Add(ctx, confHTTPActionServer); err != nil { return nil, err } diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index c0d9077c..875ec619 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -1,6 +1,8 @@ package simulate import ( + "bytes" + "compress/gzip" "context" "crypto/ecdsa" "encoding/json" @@ -63,6 +65,8 @@ type Inputs struct { EVMEventIndex int `validate:"-"` // Experimental chains support (for chains not in official chain-selectors) ExperimentalForwarders map[uint64]common.Address `validate:"-"` // forwarders keyed by chain ID + // Limits enforcement + LimitsPath string `validate:"-"` // "default" or path to custom limits JSON } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -100,6 +104,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix)") simulateCmd.Flags().String("evm-tx-hash", "", "EVM trigger transaction hash (0x...)") simulateCmd.Flags().Int("evm-event-index", -1, "EVM trigger log index (0-based)") + simulateCmd.Flags().String("limits", "default", "Production limits to enforce during simulation. Use 'default' for prod defaults, path to custom limits.json, or 'none' to disable") return simulateCmd } @@ -236,6 +241,7 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) EVMTxHash: v.GetString("evm-tx-hash"), EVMEventIndex: v.GetInt("evm-event-index"), ExperimentalForwarders: experimentalForwarders, + LimitsPath: v.GetString("limits"), }, nil } @@ -336,6 +342,40 @@ func (h *handler) Execute(inputs Inputs) error { ui.Success("Workflow compiled") } + // Resolve simulation limits + simLimits, err := ResolveLimits(inputs.LimitsPath) + if err != nil { + return fmt.Errorf("failed to resolve simulation limits: %w", err) + } + + // WASM binary size pre-flight check + if simLimits != nil { + binaryLimit := simLimits.WASMBinarySize() + if binaryLimit > 0 && len(wasmFileBinary) > binaryLimit { + return fmt.Errorf("WASM binary size %d bytes exceeds limit of %d bytes", len(wasmFileBinary), binaryLimit) + } + + compressedLimit := simLimits.WASMCompressedBinarySize() + if compressedLimit > 0 { + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write(wasmFileBinary); err != nil { + return fmt.Errorf("failed to compress WASM binary for size check: %w", err) + } + if err := gz.Close(); err != nil { + return fmt.Errorf("failed to finalize WASM compression: %w", err) + } + compressedSize := buf.Len() + if compressedSize > compressedLimit { + return fmt.Errorf("WASM compressed binary size %d bytes exceeds limit of %d bytes", compressedSize, compressedLimit) + } + } + + ui.Success("Simulation limits enabled") + ui.Dim(simLimits.LimitsSummary()) + } + + // Read the config file var config []byte if cmdcommon.IsURL(inputs.ConfigPath) { ui.Dim("Fetching config from URL...") @@ -375,7 +415,7 @@ func (h *handler) Execute(inputs Inputs) error { // if logger instance is set to DEBUG, that means verbosity flag is set by the user verbosity := h.log.GetLevel() == zerolog.DebugLevel - err = run(ctx, wasmFileBinary, config, secrets, inputs, verbosity) + err = run(ctx, wasmFileBinary, config, secrets, inputs, verbosity, simLimits) if err != nil { return err } @@ -409,6 +449,7 @@ func run( binary, config, secrets []byte, inputs Inputs, verbosity bool, + simLimits *SimulationLimits, ) error { logCfg := logger.Config{Level: getLevel(verbosity, zapcore.InfoLevel)} simLogger := NewSimulationLogger(verbosity) @@ -479,14 +520,14 @@ func run( triggerLggr := lggr.Named("TriggerCapabilities") var err error - triggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, manualTriggerCapConfig, !inputs.Broadcast) + triggerCaps, err = NewManualTriggerCapabilities(ctx, triggerLggr, registry, manualTriggerCapConfig, !inputs.Broadcast, simLimits) if err != nil { ui.Error(fmt.Sprintf("Failed to create trigger capabilities: %v", err)) os.Exit(1) } computeLggr := lggr.Named("ActionsCapabilities") - computeCaps, err := NewFakeActionCapabilities(ctx, computeLggr, registry, inputs.SecretsPath) + computeCaps, err := NewFakeActionCapabilities(ctx, computeLggr, registry, inputs.SecretsPath, simLimits) if err != nil { ui.Error(fmt.Sprintf("Failed to create compute capabilities: %v", err)) os.Exit(1) @@ -638,6 +679,11 @@ func run( commonsettings.Bool(true), // Allow all chains in simulation map[string]bool{}, ) + // Apply simulation limits to engine-level settings when --limits is set + if simLimits != nil { + applyEngineLimits(cfg, simLimits) + // Re-apply allow-all chains since applyEngineLimits does not touch ChainAllowed + } }, }) diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index 844b40d3..cf3579a1 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -8,6 +8,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/workflow/convert" "github.com/smartcontractkit/cre-cli/cmd/workflow/delete" "github.com/smartcontractkit/cre-cli/cmd/workflow/deploy" + "github.com/smartcontractkit/cre-cli/cmd/workflow/limits" "github.com/smartcontractkit/cre-cli/cmd/workflow/hash" "github.com/smartcontractkit/cre-cli/cmd/workflow/pause" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" @@ -31,6 +32,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { workflowCmd.AddCommand(deploy.New(runtimeContext)) workflowCmd.AddCommand(hash.New(runtimeContext)) workflowCmd.AddCommand(simulate.New(runtimeContext)) + workflowCmd.AddCommand(limits.New(runtimeContext)) return workflowCmd } From 0a6485b2d52165ae11f5b9f08b0ad4b78520213f Mon Sep 17 00:00:00 2001 From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:37:03 -0800 Subject: [PATCH 02/12] feat: add --limits flag to cre workflow simulate --- cmd/workflow/limits/export.go | 43 +++ cmd/workflow/simulate/limited_capabilities.go | 278 ++++++++++++++++++ cmd/workflow/simulate/limits.go | 177 +++++++++++ cmd/workflow/simulate/limits.json | 69 +++++ docs/cre_workflow.md | 1 + docs/cre_workflow_limits.md | 28 ++ docs/cre_workflow_limits_export.md | 38 +++ docs/cre_workflow_simulate.md | 1 + 8 files changed, 635 insertions(+) create mode 100644 cmd/workflow/limits/export.go create mode 100644 cmd/workflow/simulate/limited_capabilities.go create mode 100644 cmd/workflow/simulate/limits.go create mode 100644 cmd/workflow/simulate/limits.json create mode 100644 docs/cre_workflow_limits.md create mode 100644 docs/cre_workflow_limits_export.md diff --git a/cmd/workflow/limits/export.go b/cmd/workflow/limits/export.go new file mode 100644 index 00000000..ce0b6e1b --- /dev/null +++ b/cmd/workflow/limits/export.go @@ -0,0 +1,43 @@ +package limits + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" + "github.com/smartcontractkit/cre-cli/internal/runtime" +) + +func New(runtimeContext *runtime.Context) *cobra.Command { + limitsCmd := &cobra.Command{ + Use: "limits", + Short: "Manage simulation limits", + Long: `The limits command provides tools for managing workflow simulation limits.`, + } + + limitsCmd.AddCommand(newExportCmd()) + + return limitsCmd +} + +func newExportCmd() *cobra.Command { + return &cobra.Command{ + Use: "export", + Short: "Export default simulation limits as JSON", + Long: `Exports the default production simulation limits as JSON. + +The output can be redirected to a file and customized for use with +the --limits flag of the simulate command. + +Example: + cre workflow limits export > my-limits.json + cre workflow simulate ./my-workflow --limits ./my-limits.json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + data := simulate.ExportDefaultLimitsJSON() + fmt.Println(string(data)) + return nil + }, + } +} diff --git a/cmd/workflow/simulate/limited_capabilities.go b/cmd/workflow/simulate/limited_capabilities.go new file mode 100644 index 00000000..4d9110fa --- /dev/null +++ b/cmd/workflow/simulate/limited_capabilities.go @@ -0,0 +1,278 @@ +package simulate + +import ( + "context" + "fmt" + "time" + + "google.golang.org/protobuf/proto" + + commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" + confhttpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp/server" + customhttp "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/http" + httpserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/http/server" + evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + evmserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm/server" + consensusserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +// --- LimitedHTTPAction --- + +// LimitedHTTPAction wraps an httpserver.ClientCapability and enforces request/response +// size limits and connection timeout from SimulationLimits. +type LimitedHTTPAction struct { + inner httpserver.ClientCapability + limits *SimulationLimits +} + +var _ httpserver.ClientCapability = (*LimitedHTTPAction)(nil) + +func NewLimitedHTTPAction(inner httpserver.ClientCapability, limits *SimulationLimits) *LimitedHTTPAction { + return &LimitedHTTPAction{inner: inner, limits: limits} +} + +func (l *LimitedHTTPAction) SendRequest(ctx context.Context, metadata commonCap.RequestMetadata, input *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) { + // Check request body size + reqLimit := l.limits.HTTPRequestSizeLimit() + if reqLimit > 0 && len(input.GetBody()) > reqLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: HTTP request body size %d bytes exceeds limit of %d bytes", len(input.GetBody()), reqLimit), + caperrors.ResourceExhausted, + ) + } + + // Enforce connection timeout + connTimeout := l.limits.Workflows.HTTPAction.ConnectionTimeout.DefaultValue + if connTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(connTimeout)) + defer cancel() + } + + // Delegate to inner + resp, capErr := l.inner.SendRequest(ctx, metadata, input) + if capErr != nil { + return resp, capErr + } + + // Check response body size + respLimit := l.limits.HTTPResponseSizeLimit() + if resp != nil && resp.Response != nil && respLimit > 0 && len(resp.Response.GetBody()) > respLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: HTTP response body size %d bytes exceeds limit of %d bytes", len(resp.Response.GetBody()), respLimit), + caperrors.ResourceExhausted, + ) + } + + return resp, nil +} + +func (l *LimitedHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedHTTPAction) Close() error { return l.inner.Close() } +func (l *LimitedHTTPAction) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedHTTPAction) Name() string { return l.inner.Name() } +func (l *LimitedHTTPAction) Description() string { return l.inner.Description() } +func (l *LimitedHTTPAction) Ready() error { return l.inner.Ready() } +func (l *LimitedHTTPAction) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { + return l.inner.Initialise(ctx, deps) +} + +// --- LimitedConfidentialHTTPAction --- + +// LimitedConfidentialHTTPAction wraps a confhttpserver.ClientCapability and enforces +// request/response size limits and connection timeout from SimulationLimits. +type LimitedConfidentialHTTPAction struct { + inner confhttpserver.ClientCapability + limits *SimulationLimits +} + +var _ confhttpserver.ClientCapability = (*LimitedConfidentialHTTPAction)(nil) + +func NewLimitedConfidentialHTTPAction(inner confhttpserver.ClientCapability, limits *SimulationLimits) *LimitedConfidentialHTTPAction { + return &LimitedConfidentialHTTPAction{inner: inner, limits: limits} +} + +func (l *LimitedConfidentialHTTPAction) SendRequest(ctx context.Context, metadata commonCap.RequestMetadata, input *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) { + // Check request size (body string or body bytes) + reqLimit := l.limits.ConfHTTPRequestSizeLimit() + if reqLimit > 0 && input.GetRequest() != nil { + reqSize := len(input.GetRequest().GetBodyString()) + len(input.GetRequest().GetBodyBytes()) + if reqSize > reqLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: confidential HTTP request body size %d bytes exceeds limit of %d bytes", reqSize, reqLimit), + caperrors.ResourceExhausted, + ) + } + } + + // Enforce connection timeout + connTimeout := l.limits.Workflows.ConfidentialHTTP.ConnectionTimeout.DefaultValue + if connTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(connTimeout)) + defer cancel() + } + + // Delegate to inner + resp, capErr := l.inner.SendRequest(ctx, metadata, input) + if capErr != nil { + return resp, capErr + } + + // Check response body size + respLimit := l.limits.ConfHTTPResponseSizeLimit() + if resp != nil && resp.Response != nil && respLimit > 0 && len(resp.Response.GetBody()) > respLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: confidential HTTP response body size %d bytes exceeds limit of %d bytes", len(resp.Response.GetBody()), respLimit), + caperrors.ResourceExhausted, + ) + } + + return resp, nil +} + +func (l *LimitedConfidentialHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedConfidentialHTTPAction) Close() error { return l.inner.Close() } +func (l *LimitedConfidentialHTTPAction) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedConfidentialHTTPAction) Name() string { return l.inner.Name() } +func (l *LimitedConfidentialHTTPAction) Description() string { return l.inner.Description() } +func (l *LimitedConfidentialHTTPAction) Ready() error { return l.inner.Ready() } +func (l *LimitedConfidentialHTTPAction) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { + return l.inner.Initialise(ctx, deps) +} + +// --- LimitedConsensusNoDAG --- + +// LimitedConsensusNoDAG wraps a consensusserver.ConsensusCapability and enforces +// observation size limits from SimulationLimits. +type LimitedConsensusNoDAG struct { + inner consensusserver.ConsensusCapability + limits *SimulationLimits +} + +var _ consensusserver.ConsensusCapability = (*LimitedConsensusNoDAG)(nil) + +func NewLimitedConsensusNoDAG(inner consensusserver.ConsensusCapability, limits *SimulationLimits) *LimitedConsensusNoDAG { + return &LimitedConsensusNoDAG{inner: inner, limits: limits} +} + +func (l *LimitedConsensusNoDAG) Simple(ctx context.Context, metadata commonCap.RequestMetadata, input *sdkpb.SimpleConsensusInputs) (*commonCap.ResponseAndMetadata[*valuespb.Value], caperrors.Error) { + // Check observation size + obsLimit := l.limits.ConsensusObservationSizeLimit() + if obsLimit > 0 { + inputSize := proto.Size(input) + if inputSize > obsLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: consensus observation size %d bytes exceeds limit of %d bytes", inputSize, obsLimit), + caperrors.ResourceExhausted, + ) + } + } + + return l.inner.Simple(ctx, metadata, input) +} + +func (l *LimitedConsensusNoDAG) Report(ctx context.Context, metadata commonCap.RequestMetadata, input *sdkpb.ReportRequest) (*commonCap.ResponseAndMetadata[*sdkpb.ReportResponse], caperrors.Error) { + // Report size is engine-enforced, delegate as-is + return l.inner.Report(ctx, metadata, input) +} + +func (l *LimitedConsensusNoDAG) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedConsensusNoDAG) Close() error { return l.inner.Close() } +func (l *LimitedConsensusNoDAG) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedConsensusNoDAG) Name() string { return l.inner.Name() } +func (l *LimitedConsensusNoDAG) Description() string { return l.inner.Description() } +func (l *LimitedConsensusNoDAG) Ready() error { return l.inner.Ready() } +func (l *LimitedConsensusNoDAG) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { + return l.inner.Initialise(ctx, deps) +} + +// --- LimitedEVMChain --- + +// LimitedEVMChain wraps an evmserver.ClientCapability and enforces chain write +// report size and gas limits from SimulationLimits. +type LimitedEVMChain struct { + inner evmserver.ClientCapability + limits *SimulationLimits +} + +var _ evmserver.ClientCapability = (*LimitedEVMChain)(nil) + +func NewLimitedEVMChain(inner evmserver.ClientCapability, limits *SimulationLimits) *LimitedEVMChain { + return &LimitedEVMChain{inner: inner, limits: limits} +} + +func (l *LimitedEVMChain) WriteReport(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) { + // Check report size + reportLimit := l.limits.ChainWriteReportSizeLimit() + if reportLimit > 0 && input.Report != nil && len(input.Report.RawReport) > reportLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), reportLimit), + caperrors.ResourceExhausted, + ) + } + + // Check gas limit + gasLimit := l.limits.ChainWriteEVMGasLimit() + if gasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > gasLimit { + return nil, caperrors.NewPublicUserError( + fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, gasLimit), + caperrors.ResourceExhausted, + ) + } + + return l.inner.WriteReport(ctx, metadata, input) +} + +// All other methods delegate to the inner capability. +func (l *LimitedEVMChain) CallContract(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.CallContractRequest) (*commonCap.ResponseAndMetadata[*evmcappb.CallContractReply], caperrors.Error) { + return l.inner.CallContract(ctx, metadata, input) +} + +func (l *LimitedEVMChain) FilterLogs(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.FilterLogsRequest) (*commonCap.ResponseAndMetadata[*evmcappb.FilterLogsReply], caperrors.Error) { + return l.inner.FilterLogs(ctx, metadata, input) +} + +func (l *LimitedEVMChain) BalanceAt(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.BalanceAtRequest) (*commonCap.ResponseAndMetadata[*evmcappb.BalanceAtReply], caperrors.Error) { + return l.inner.BalanceAt(ctx, metadata, input) +} + +func (l *LimitedEVMChain) EstimateGas(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.EstimateGasRequest) (*commonCap.ResponseAndMetadata[*evmcappb.EstimateGasReply], caperrors.Error) { + return l.inner.EstimateGas(ctx, metadata, input) +} + +func (l *LimitedEVMChain) GetTransactionByHash(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.GetTransactionByHashRequest) (*commonCap.ResponseAndMetadata[*evmcappb.GetTransactionByHashReply], caperrors.Error) { + return l.inner.GetTransactionByHash(ctx, metadata, input) +} + +func (l *LimitedEVMChain) GetTransactionReceipt(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.GetTransactionReceiptRequest) (*commonCap.ResponseAndMetadata[*evmcappb.GetTransactionReceiptReply], caperrors.Error) { + return l.inner.GetTransactionReceipt(ctx, metadata, input) +} + +func (l *LimitedEVMChain) HeaderByNumber(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.HeaderByNumberRequest) (*commonCap.ResponseAndMetadata[*evmcappb.HeaderByNumberReply], caperrors.Error) { + return l.inner.HeaderByNumber(ctx, metadata, input) +} + +func (l *LimitedEVMChain) RegisterLogTrigger(ctx context.Context, triggerID string, metadata commonCap.RequestMetadata, input *evmcappb.FilterLogTriggerRequest) (<-chan commonCap.TriggerAndId[*evmcappb.Log], caperrors.Error) { + return l.inner.RegisterLogTrigger(ctx, triggerID, metadata, input) +} + +func (l *LimitedEVMChain) UnregisterLogTrigger(ctx context.Context, triggerID string, metadata commonCap.RequestMetadata, input *evmcappb.FilterLogTriggerRequest) caperrors.Error { + return l.inner.UnregisterLogTrigger(ctx, triggerID, metadata, input) +} + +func (l *LimitedEVMChain) ChainSelector() uint64 { return l.inner.ChainSelector() } +func (l *LimitedEVMChain) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedEVMChain) Close() error { return l.inner.Close() } +func (l *LimitedEVMChain) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedEVMChain) Name() string { return l.inner.Name() } +func (l *LimitedEVMChain) Description() string { return l.inner.Description() } +func (l *LimitedEVMChain) Ready() error { return l.inner.Ready() } +func (l *LimitedEVMChain) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { + return l.inner.Initialise(ctx, deps) +} diff --git a/cmd/workflow/simulate/limits.go b/cmd/workflow/simulate/limits.go new file mode 100644 index 00000000..2c273bf4 --- /dev/null +++ b/cmd/workflow/simulate/limits.go @@ -0,0 +1,177 @@ +package simulate + +import ( + _ "embed" + "encoding/json" + "fmt" + "os" + + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" +) + +//go:embed limits.json +var defaultLimitsJSON []byte + +// SimulationLimits holds the workflow-level limits applied during simulation. +type SimulationLimits struct { + Workflows cresettings.Workflows +} + +// DefaultLimits returns simulation limits populated from the embedded defaults. +func DefaultLimits() (*SimulationLimits, error) { + return parseLimitsJSON(defaultLimitsJSON) +} + +// LoadLimits reads a limits JSON file from disk and returns parsed SimulationLimits. +func LoadLimits(path string) (*SimulationLimits, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read limits file %s: %w", path, err) + } + return parseLimitsJSON(data) +} + +func parseLimitsJSON(data []byte) (*SimulationLimits, error) { + // Start from the pre-built Default which has all Parse functions configured. + // Setting[T].Parse is a function closure (json:"-") that cannot be serialized, + // so we must unmarshal into a copy that already has Parse funcs set. + w := cresettings.Default.PerWorkflow + if err := json.Unmarshal(data, &w); err != nil { + return nil, fmt.Errorf("failed to parse limits JSON: %w", err) + } + return &SimulationLimits{Workflows: w}, nil +} + +// applyEngineLimits copies limit values from the SimulationLimits into the +// engine's workflow settings config. ChainAllowed is intentionally left as +// allow-all for simulation. +func applyEngineLimits(cfg *cresettings.Workflows, limits *SimulationLimits) { + src := &limits.Workflows + + // Execution limits + cfg.ExecutionTimeout = src.ExecutionTimeout + cfg.ExecutionResponseLimit = src.ExecutionResponseLimit + cfg.ExecutionConcurrencyLimit = src.ExecutionConcurrencyLimit + + // Capability limits + cfg.CapabilityConcurrencyLimit = src.CapabilityConcurrencyLimit + cfg.CapabilityCallTimeout = src.CapabilityCallTimeout + cfg.SecretsConcurrencyLimit = src.SecretsConcurrencyLimit + + // Trigger limits + cfg.TriggerRegistrationsTimeout = src.TriggerRegistrationsTimeout + cfg.TriggerEventQueueLimit = src.TriggerEventQueueLimit + cfg.TriggerEventQueueTimeout = src.TriggerEventQueueTimeout + cfg.TriggerSubscriptionTimeout = src.TriggerSubscriptionTimeout + cfg.TriggerSubscriptionLimit = src.TriggerSubscriptionLimit + + // WASM limits + cfg.WASMMemoryLimit = src.WASMMemoryLimit + cfg.WASMBinarySizeLimit = src.WASMBinarySizeLimit + cfg.WASMCompressedBinarySizeLimit = src.WASMCompressedBinarySizeLimit + cfg.WASMConfigSizeLimit = src.WASMConfigSizeLimit + cfg.WASMSecretsSizeLimit = src.WASMSecretsSizeLimit + + // Log limits + cfg.LogLineLimit = src.LogLineLimit + cfg.LogEventLimit = src.LogEventLimit + + // Call count limits + cfg.ChainRead = src.ChainRead + cfg.ChainWrite.TargetsLimit = src.ChainWrite.TargetsLimit + cfg.Consensus.CallLimit = src.Consensus.CallLimit + cfg.HTTPAction.CallLimit = src.HTTPAction.CallLimit + cfg.ConfidentialHTTP.CallLimit = src.ConfidentialHTTP.CallLimit + cfg.Secrets = src.Secrets + + // Trigger-specific limits + cfg.CRONTrigger = src.CRONTrigger + cfg.HTTPTrigger = src.HTTPTrigger + cfg.LogTrigger = src.LogTrigger + + // NOTE: ChainAllowed is NOT overridden — simulation keeps allow-all +} + +// HTTPRequestSizeLimit returns the HTTP action request size limit in bytes. +func (l *SimulationLimits) HTTPRequestSizeLimit() int { + return int(l.Workflows.HTTPAction.RequestSizeLimit.DefaultValue) +} + +// HTTPResponseSizeLimit returns the HTTP action response size limit in bytes. +func (l *SimulationLimits) HTTPResponseSizeLimit() int { + return int(l.Workflows.HTTPAction.ResponseSizeLimit.DefaultValue) +} + +// ConfHTTPRequestSizeLimit returns the confidential HTTP request size limit in bytes. +func (l *SimulationLimits) ConfHTTPRequestSizeLimit() int { + return int(l.Workflows.ConfidentialHTTP.RequestSizeLimit.DefaultValue) +} + +// ConfHTTPResponseSizeLimit returns the confidential HTTP response size limit in bytes. +func (l *SimulationLimits) ConfHTTPResponseSizeLimit() int { + return int(l.Workflows.ConfidentialHTTP.ResponseSizeLimit.DefaultValue) +} + +// ConsensusObservationSizeLimit returns the consensus observation size limit in bytes. +func (l *SimulationLimits) ConsensusObservationSizeLimit() int { + return int(l.Workflows.Consensus.ObservationSizeLimit.DefaultValue) +} + +// ChainWriteReportSizeLimit returns the chain write report size limit in bytes. +func (l *SimulationLimits) ChainWriteReportSizeLimit() int { + return int(l.Workflows.ChainWrite.ReportSizeLimit.DefaultValue) +} + +// ChainWriteEVMGasLimit returns the default EVM gas limit. +func (l *SimulationLimits) ChainWriteEVMGasLimit() uint64 { + return l.Workflows.ChainWrite.EVM.GasLimit.Default.DefaultValue +} + +// WASMBinarySize returns the WASM binary size limit in bytes. +func (l *SimulationLimits) WASMBinarySize() int { + return int(l.Workflows.WASMBinarySizeLimit.DefaultValue) +} + +// WASMCompressedBinarySize returns the WASM compressed binary size limit in bytes. +func (l *SimulationLimits) WASMCompressedBinarySize() int { + return int(l.Workflows.WASMCompressedBinarySizeLimit.DefaultValue) +} + +// LimitsSummary returns a human-readable summary of key limits. +func (l *SimulationLimits) LimitsSummary() string { + w := &l.Workflows + return fmt.Sprintf( + "HTTP: req=%s resp=%s timeout=%s | ConfHTTP: req=%s resp=%s timeout=%s | Consensus obs=%s | ChainWrite report=%s gas=%d | WASM binary=%s compressed=%s", + w.HTTPAction.RequestSizeLimit.DefaultValue, + w.HTTPAction.ResponseSizeLimit.DefaultValue, + w.HTTPAction.ConnectionTimeout.DefaultValue, + w.ConfidentialHTTP.RequestSizeLimit.DefaultValue, + w.ConfidentialHTTP.ResponseSizeLimit.DefaultValue, + w.ConfidentialHTTP.ConnectionTimeout.DefaultValue, + w.Consensus.ObservationSizeLimit.DefaultValue, + w.ChainWrite.ReportSizeLimit.DefaultValue, + w.ChainWrite.EVM.GasLimit.Default.DefaultValue, + w.WASMBinarySizeLimit.DefaultValue, + w.WASMCompressedBinarySizeLimit.DefaultValue, + ) +} + +// ExportDefaultLimitsJSON returns the embedded default limits JSON. +func ExportDefaultLimitsJSON() []byte { + return defaultLimitsJSON +} + +// ResolveLimits resolves a --limits flag value to SimulationLimits. +// Returns nil if limitsFlag is "none" (no limits enforcement). +func ResolveLimits(limitsFlag string) (*SimulationLimits, error) { + if limitsFlag == "" || limitsFlag == "none" { + return nil, nil + } + + if limitsFlag == "default" { + return DefaultLimits() + } + + return LoadLimits(limitsFlag) +} + diff --git a/cmd/workflow/simulate/limits.json b/cmd/workflow/simulate/limits.json new file mode 100644 index 00000000..ced46eeb --- /dev/null +++ b/cmd/workflow/simulate/limits.json @@ -0,0 +1,69 @@ +{ + "TriggerRegistrationsTimeout": "10s", + "TriggerSubscriptionTimeout": "15s", + "TriggerSubscriptionLimit": "10", + "TriggerEventQueueLimit": "50", + "TriggerEventQueueTimeout": "10m0s", + "CapabilityConcurrencyLimit": "30", + "CapabilityCallTimeout": "3m0s", + "SecretsConcurrencyLimit": "5", + "ExecutionConcurrencyLimit": "5", + "ExecutionTimeout": "5m0s", + "ExecutionResponseLimit": "100kb", + "ExecutionTimestampsEnabled": "false", + "WASMMemoryLimit": "100mb", + "WASMBinarySizeLimit": "100mb", + "WASMCompressedBinarySizeLimit": "20mb", + "WASMConfigSizeLimit": "1mb", + "WASMSecretsSizeLimit": "1mb", + "LogLineLimit": "1kb", + "LogEventLimit": "1000", + "CRONTrigger": { + "FastestScheduleInterval": "30s" + }, + "HTTPTrigger": { + "RateLimit": "every30s:3" + }, + "LogTrigger": { + "EventRateLimit": "every6s:10", + "EventSizeLimit": "5kb", + "FilterAddressLimit": "5", + "FilterTopicsPerSlotLimit": "10" + }, + "ChainWrite": { + "TargetsLimit": "10", + "ReportSizeLimit": "5kb", + "EVM": { + "TransactionGasLimit": "5000000", + "GasLimit": { + "Default": "5000000", + "Values": {} + } + } + }, + "ChainRead": { + "CallLimit": "15", + "LogQueryBlockLimit": "100", + "PayloadSizeLimit": "5kb" + }, + "Consensus": { + "ObservationSizeLimit": "100kb", + "CallLimit": "20" + }, + "HTTPAction": { + "CallLimit": "5", + "CacheAgeLimit": "10m0s", + "ConnectionTimeout": "10s", + "RequestSizeLimit": "10kb", + "ResponseSizeLimit": "100kb" + }, + "ConfidentialHTTP": { + "CallLimit": "5", + "ConnectionTimeout": "10s", + "RequestSizeLimit": "10kb", + "ResponseSizeLimit": "100kb" + }, + "Secrets": { + "CallLimit": "5" + } +} diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index 1b220e78..da258ffe 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -34,6 +34,7 @@ cre workflow [optional flags] * [cre workflow custom-build](cre_workflow_custom-build.md) - Converts an existing workflow to a custom (self-compiled) build * [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry * [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract +* [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits * [cre workflow hash](cre_workflow_hash.md) - Computes and displays workflow hashes * [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract * [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow diff --git a/docs/cre_workflow_limits.md b/docs/cre_workflow_limits.md new file mode 100644 index 00000000..4367f1b6 --- /dev/null +++ b/docs/cre_workflow_limits.md @@ -0,0 +1,28 @@ +## cre workflow limits + +Manage simulation limits + +### Synopsis + +The limits command provides tools for managing workflow simulation limits. + +### Options + +``` + -h, --help help for limits +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info (default ".env") + -R, --project-root string Path to the project root + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre workflow](cre_workflow.md) - Manages workflows +* [cre workflow limits export](cre_workflow_limits_export.md) - Export default simulation limits as JSON + diff --git a/docs/cre_workflow_limits_export.md b/docs/cre_workflow_limits_export.md new file mode 100644 index 00000000..a1028b06 --- /dev/null +++ b/docs/cre_workflow_limits_export.md @@ -0,0 +1,38 @@ +## cre workflow limits export + +Export default simulation limits as JSON + +### Synopsis + +Exports the default production simulation limits as JSON. + +The output can be redirected to a file and customized for use with +the --limits flag of the simulate command. + +Example: + cre workflow limits export > my-limits.json + cre workflow simulate ./my-workflow --limits ./my-limits.json + +``` +cre workflow limits export [optional flags] +``` + +### Options + +``` + -h, --help help for export +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info (default ".env") + -R, --project-root string Path to the project root + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits + diff --git a/docs/cre_workflow_simulate.md b/docs/cre_workflow_simulate.md index 086c389e..07514573 100644 --- a/docs/cre_workflow_simulate.md +++ b/docs/cre_workflow_simulate.md @@ -27,6 +27,7 @@ cre workflow simulate ./my-workflow --evm-tx-hash string EVM trigger transaction hash (0x...) -h, --help help for simulate --http-payload string HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix) + --limits string Production limits to enforce during simulation. Use 'default' for prod defaults, path to custom limits.json, or 'none' to disable (default "default") --no-config Simulate without a config file --non-interactive Run without prompts; requires --trigger-index and inputs for the selected trigger type --trigger-index int Index of the trigger to run (0-based) (default -1) From eabd8b51cc05695d75c47d791b8371ec7f65a0b9 Mon Sep 17 00:00:00 2001 From: De Clercq Wentzel <10665586+wentzeld@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:47:58 -0800 Subject: [PATCH 03/12] feat: add --limits flag to cre workflow simulate --- cmd/root.go | 110 +++++++++--------- cmd/workflow/simulate/limited_capabilities.go | 52 +++++---- cmd/workflow/simulate/limits.go | 1 - 3 files changed, 82 insertions(+), 81 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 11bfd37a..25f14b54 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -421,32 +421,32 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account access": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account access": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, "cre workflow custom-build": {}, "cre workflow limits": {}, "cre workflow limits export": {}, "cre workflow build": {}, - "cre account": {}, - "cre secrets": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre": {}, + "cre account": {}, + "cre secrets": {}, + "cre templates": {}, + "cre templates list": {}, + "cre templates add": {}, + "cre templates remove": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] @@ -456,28 +456,28 @@ func isLoadSettings(cmd *cobra.Command) bool { func isLoadCredentials(cmd *cobra.Command) bool { // It is not expected to have the credentials loaded when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre generate-bindings": {}, - "cre update": {}, - "cre workflow": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre generate-bindings": {}, + "cre update": {}, + "cre workflow": {}, "cre workflow limits": {}, "cre workflow limits export": {}, "cre account": {}, "cre secrets": {}, "cre workflow build": {}, "cre workflow hash": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre": {}, + "cre templates": {}, + "cre templates list": {}, + "cre templates add": {}, + "cre templates remove": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] @@ -528,28 +528,28 @@ func shouldShowSpinner(cmd *cobra.Command) bool { // Don't show spinner for commands that don't do async work // or commands that have their own interactive UI (like init) var excludedCommands = map[string]struct{}{ - "cre": {}, - "cre version": {}, - "cre help": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre init": {}, // Has its own Huh forms UI - "cre login": {}, // Has its own interactive flow - "cre logout": {}, - "cre update": {}, - "cre workflow": {}, // Just shows help + "cre": {}, + "cre version": {}, + "cre help": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre init": {}, // Has its own Huh forms UI + "cre login": {}, // Has its own interactive flow + "cre logout": {}, + "cre update": {}, + "cre workflow": {}, // Just shows help "cre workflow limits": {}, // Just shows help "cre workflow limits export": {}, // Static data, no project needed "cre account": {}, // Just shows help "cre workflow build": {}, // Offline command, no async init "cre workflow hash": {}, // Offline command, has own spinner - "cre secrets": {}, // Just shows help - "cre templates": {}, // Just shows help - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, + "cre secrets": {}, // Just shows help + "cre templates": {}, // Just shows help + "cre templates list": {}, + "cre templates add": {}, + "cre templates remove": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/simulate/limited_capabilities.go b/cmd/workflow/simulate/limited_capabilities.go index 4d9110fa..ec9a834d 100644 --- a/cmd/workflow/simulate/limited_capabilities.go +++ b/cmd/workflow/simulate/limited_capabilities.go @@ -16,9 +16,9 @@ import ( evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" evmserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm/server" consensusserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/consensus/server" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" ) // --- LimitedHTTPAction --- @@ -72,12 +72,12 @@ func (l *LimitedHTTPAction) SendRequest(ctx context.Context, metadata commonCap. return resp, nil } -func (l *LimitedHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } -func (l *LimitedHTTPAction) Close() error { return l.inner.Close() } -func (l *LimitedHTTPAction) HealthReport() map[string]error { return l.inner.HealthReport() } -func (l *LimitedHTTPAction) Name() string { return l.inner.Name() } -func (l *LimitedHTTPAction) Description() string { return l.inner.Description() } -func (l *LimitedHTTPAction) Ready() error { return l.inner.Ready() } +func (l *LimitedHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedHTTPAction) Close() error { return l.inner.Close() } +func (l *LimitedHTTPAction) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedHTTPAction) Name() string { return l.inner.Name() } +func (l *LimitedHTTPAction) Description() string { return l.inner.Description() } +func (l *LimitedHTTPAction) Ready() error { return l.inner.Ready() } func (l *LimitedHTTPAction) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { return l.inner.Initialise(ctx, deps) } @@ -136,12 +136,14 @@ func (l *LimitedConfidentialHTTPAction) SendRequest(ctx context.Context, metadat return resp, nil } -func (l *LimitedConfidentialHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } -func (l *LimitedConfidentialHTTPAction) Close() error { return l.inner.Close() } -func (l *LimitedConfidentialHTTPAction) HealthReport() map[string]error { return l.inner.HealthReport() } -func (l *LimitedConfidentialHTTPAction) Name() string { return l.inner.Name() } -func (l *LimitedConfidentialHTTPAction) Description() string { return l.inner.Description() } -func (l *LimitedConfidentialHTTPAction) Ready() error { return l.inner.Ready() } +func (l *LimitedConfidentialHTTPAction) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedConfidentialHTTPAction) Close() error { return l.inner.Close() } +func (l *LimitedConfidentialHTTPAction) HealthReport() map[string]error { + return l.inner.HealthReport() +} +func (l *LimitedConfidentialHTTPAction) Name() string { return l.inner.Name() } +func (l *LimitedConfidentialHTTPAction) Description() string { return l.inner.Description() } +func (l *LimitedConfidentialHTTPAction) Ready() error { return l.inner.Ready() } func (l *LimitedConfidentialHTTPAction) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { return l.inner.Initialise(ctx, deps) } @@ -182,12 +184,12 @@ func (l *LimitedConsensusNoDAG) Report(ctx context.Context, metadata commonCap.R return l.inner.Report(ctx, metadata, input) } -func (l *LimitedConsensusNoDAG) Start(ctx context.Context) error { return l.inner.Start(ctx) } -func (l *LimitedConsensusNoDAG) Close() error { return l.inner.Close() } -func (l *LimitedConsensusNoDAG) HealthReport() map[string]error { return l.inner.HealthReport() } -func (l *LimitedConsensusNoDAG) Name() string { return l.inner.Name() } -func (l *LimitedConsensusNoDAG) Description() string { return l.inner.Description() } -func (l *LimitedConsensusNoDAG) Ready() error { return l.inner.Ready() } +func (l *LimitedConsensusNoDAG) Start(ctx context.Context) error { return l.inner.Start(ctx) } +func (l *LimitedConsensusNoDAG) Close() error { return l.inner.Close() } +func (l *LimitedConsensusNoDAG) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedConsensusNoDAG) Name() string { return l.inner.Name() } +func (l *LimitedConsensusNoDAG) Description() string { return l.inner.Description() } +func (l *LimitedConsensusNoDAG) Ready() error { return l.inner.Ready() } func (l *LimitedConsensusNoDAG) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { return l.inner.Initialise(ctx, deps) } @@ -266,13 +268,13 @@ func (l *LimitedEVMChain) UnregisterLogTrigger(ctx context.Context, triggerID st return l.inner.UnregisterLogTrigger(ctx, triggerID, metadata, input) } -func (l *LimitedEVMChain) ChainSelector() uint64 { return l.inner.ChainSelector() } +func (l *LimitedEVMChain) ChainSelector() uint64 { return l.inner.ChainSelector() } func (l *LimitedEVMChain) Start(ctx context.Context) error { return l.inner.Start(ctx) } -func (l *LimitedEVMChain) Close() error { return l.inner.Close() } -func (l *LimitedEVMChain) HealthReport() map[string]error { return l.inner.HealthReport() } -func (l *LimitedEVMChain) Name() string { return l.inner.Name() } -func (l *LimitedEVMChain) Description() string { return l.inner.Description() } -func (l *LimitedEVMChain) Ready() error { return l.inner.Ready() } +func (l *LimitedEVMChain) Close() error { return l.inner.Close() } +func (l *LimitedEVMChain) HealthReport() map[string]error { return l.inner.HealthReport() } +func (l *LimitedEVMChain) Name() string { return l.inner.Name() } +func (l *LimitedEVMChain) Description() string { return l.inner.Description() } +func (l *LimitedEVMChain) Ready() error { return l.inner.Ready() } func (l *LimitedEVMChain) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { return l.inner.Initialise(ctx, deps) } diff --git a/cmd/workflow/simulate/limits.go b/cmd/workflow/simulate/limits.go index 2c273bf4..5c146c87 100644 --- a/cmd/workflow/simulate/limits.go +++ b/cmd/workflow/simulate/limits.go @@ -174,4 +174,3 @@ func ResolveLimits(limitsFlag string) (*SimulationLimits, error) { return LoadLimits(limitsFlag) } - From c12b87071f5d4cab6c1a5278280492ecfb27f93e Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Tue, 17 Mar 2026 14:10:41 -0300 Subject: [PATCH 04/12] fix lint --- cmd/root.go | 10 +++++----- cmd/workflow/workflow.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 25f14b54..8e8b52ba 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -439,7 +439,7 @@ func isLoadSettings(cmd *cobra.Command) bool { "cre workflow custom-build": {}, "cre workflow limits": {}, "cre workflow limits export": {}, - "cre workflow build": {}, + "cre workflow build": {}, "cre account": {}, "cre secrets": {}, "cre templates": {}, @@ -471,8 +471,8 @@ func isLoadCredentials(cmd *cobra.Command) bool { "cre workflow limits export": {}, "cre account": {}, "cre secrets": {}, - "cre workflow build": {}, - "cre workflow hash": {}, + "cre workflow build": {}, + "cre workflow hash": {}, "cre templates": {}, "cre templates list": {}, "cre templates add": {}, @@ -543,8 +543,8 @@ func shouldShowSpinner(cmd *cobra.Command) bool { "cre workflow limits": {}, // Just shows help "cre workflow limits export": {}, // Static data, no project needed "cre account": {}, // Just shows help - "cre workflow build": {}, // Offline command, no async init - "cre workflow hash": {}, // Offline command, has own spinner + "cre workflow build": {}, // Offline command, no async init + "cre workflow hash": {}, // Offline command, has own spinner "cre secrets": {}, // Just shows help "cre templates": {}, // Just shows help "cre templates list": {}, diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index cf3579a1..96ea33c4 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -8,8 +8,8 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/workflow/convert" "github.com/smartcontractkit/cre-cli/cmd/workflow/delete" "github.com/smartcontractkit/cre-cli/cmd/workflow/deploy" - "github.com/smartcontractkit/cre-cli/cmd/workflow/limits" "github.com/smartcontractkit/cre-cli/cmd/workflow/hash" + "github.com/smartcontractkit/cre-cli/cmd/workflow/limits" "github.com/smartcontractkit/cre-cli/cmd/workflow/pause" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" "github.com/smartcontractkit/cre-cli/cmd/workflow/test" From 21dd268514ec846459009370644e8938f0406d96 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Tue, 17 Mar 2026 14:10:51 -0300 Subject: [PATCH 05/12] fix implement interface --- cmd/workflow/simulate/limited_capabilities.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/workflow/simulate/limited_capabilities.go b/cmd/workflow/simulate/limited_capabilities.go index ec9a834d..3a48a850 100644 --- a/cmd/workflow/simulate/limited_capabilities.go +++ b/cmd/workflow/simulate/limited_capabilities.go @@ -278,3 +278,7 @@ func (l *LimitedEVMChain) Ready() error { return l.inner.Read func (l *LimitedEVMChain) Initialise(ctx context.Context, deps core.StandardCapabilitiesDependencies) error { return l.inner.Initialise(ctx, deps) } + +func (l *LimitedEVMChain) AckEvent(ctx context.Context, triggerId string, eventId string, method string) caperrors.Error { + return l.inner.AckEvent(ctx, triggerId, eventId, method) +} From 33f71d17b66784e34108f36e86a5d171216531f5 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Tue, 17 Mar 2026 14:11:09 -0300 Subject: [PATCH 06/12] update docs --- docs/cre_workflow.md | 2 +- docs/cre_workflow_limits.md | 3 ++- docs/cre_workflow_limits_export.md | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index da258ffe..0bdfb9a6 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -34,8 +34,8 @@ cre workflow [optional flags] * [cre workflow custom-build](cre_workflow_custom-build.md) - Converts an existing workflow to a custom (self-compiled) build * [cre workflow delete](cre_workflow_delete.md) - Deletes all versions of a workflow from the Workflow Registry * [cre workflow deploy](cre_workflow_deploy.md) - Deploys a workflow to the Workflow Registry contract -* [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits * [cre workflow hash](cre_workflow_hash.md) - Computes and displays workflow hashes +* [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits * [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract * [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow diff --git a/docs/cre_workflow_limits.md b/docs/cre_workflow_limits.md index 4367f1b6..05605bfe 100644 --- a/docs/cre_workflow_limits.md +++ b/docs/cre_workflow_limits.md @@ -15,8 +15,9 @@ The limits command provides tools for managing workflow simulation limits. ### Options inherited from parent commands ``` - -e, --env string Path to .env file which contains sensitive info (default ".env") + -e, --env string Path to .env file which contains sensitive info -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config -T, --target string Use target settings from YAML config -v, --verbose Run command in VERBOSE mode ``` diff --git a/docs/cre_workflow_limits_export.md b/docs/cre_workflow_limits_export.md index a1028b06..53f4fb6b 100644 --- a/docs/cre_workflow_limits_export.md +++ b/docs/cre_workflow_limits_export.md @@ -26,8 +26,9 @@ cre workflow limits export [optional flags] ### Options inherited from parent commands ``` - -e, --env string Path to .env file which contains sensitive info (default ".env") + -e, --env string Path to .env file which contains sensitive info -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config -T, --target string Use target settings from YAML config -v, --verbose Run command in VERBOSE mode ``` From 00791ac49ceb75dd21cc9270d1c6e1a00ff54b6e Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Wed, 18 Mar 2026 08:13:34 -0300 Subject: [PATCH 07/12] update text for sim limits --- cmd/workflow/limits/export.go | 11 +++-------- cmd/workflow/simulate/simulate.go | 2 +- docs/cre_workflow_limits_export.md | 12 ++++++------ docs/cre_workflow_simulate.md | 2 +- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/cmd/workflow/limits/export.go b/cmd/workflow/limits/export.go index ce0b6e1b..dd1a6b2f 100644 --- a/cmd/workflow/limits/export.go +++ b/cmd/workflow/limits/export.go @@ -26,14 +26,9 @@ func newExportCmd() *cobra.Command { Use: "export", Short: "Export default simulation limits as JSON", Long: `Exports the default production simulation limits as JSON. - -The output can be redirected to a file and customized for use with -the --limits flag of the simulate command. - -Example: - cre workflow limits export > my-limits.json - cre workflow simulate ./my-workflow --limits ./my-limits.json`, - Args: cobra.NoArgs, +The output can be redirected to a file and customized.`, + Example: `cre workflow limits export > my-limits.json`, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { data := simulate.ExportDefaultLimitsJSON() fmt.Println(string(data)) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 6e8d4c57..dc69e951 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -104,7 +104,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().String("http-payload", "", "HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix)") simulateCmd.Flags().String("evm-tx-hash", "", "EVM trigger transaction hash (0x...)") simulateCmd.Flags().Int("evm-event-index", -1, "EVM trigger log index (0-based)") - simulateCmd.Flags().String("limits", "default", "Production limits to enforce during simulation. Use 'default' for prod defaults, path to custom limits.json, or 'none' to disable") + simulateCmd.Flags().String("limits", "default", "Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable") return simulateCmd } diff --git a/docs/cre_workflow_limits_export.md b/docs/cre_workflow_limits_export.md index 53f4fb6b..1162a8fc 100644 --- a/docs/cre_workflow_limits_export.md +++ b/docs/cre_workflow_limits_export.md @@ -5,16 +5,16 @@ Export default simulation limits as JSON ### Synopsis Exports the default production simulation limits as JSON. +The output can be redirected to a file and customized. -The output can be redirected to a file and customized for use with -the --limits flag of the simulate command. +``` +cre workflow limits export [optional flags] +``` -Example: - cre workflow limits export > my-limits.json - cre workflow simulate ./my-workflow --limits ./my-limits.json +### Examples ``` -cre workflow limits export [optional flags] +cre workflow limits export > my-limits.json ``` ### Options diff --git a/docs/cre_workflow_simulate.md b/docs/cre_workflow_simulate.md index 07514573..ce844fd2 100644 --- a/docs/cre_workflow_simulate.md +++ b/docs/cre_workflow_simulate.md @@ -27,7 +27,7 @@ cre workflow simulate ./my-workflow --evm-tx-hash string EVM trigger transaction hash (0x...) -h, --help help for simulate --http-payload string HTTP trigger payload as JSON string or path to JSON file (with or without @ prefix) - --limits string Production limits to enforce during simulation. Use 'default' for prod defaults, path to custom limits.json, or 'none' to disable (default "default") + --limits string Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable (default "default") --no-config Simulate without a config file --non-interactive Run without prompts; requires --trigger-index and inputs for the selected trigger type --trigger-index int Index of the trigger to run (0-based) (default -1) From 75432520c76106349ed6289a9c1f330892b2e9e4 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 19 Mar 2026 11:56:02 -0300 Subject: [PATCH 08/12] remove unused arg --- cmd/workflow/limits/export.go | 3 +-- cmd/workflow/workflow.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cmd/workflow/limits/export.go b/cmd/workflow/limits/export.go index dd1a6b2f..4a206857 100644 --- a/cmd/workflow/limits/export.go +++ b/cmd/workflow/limits/export.go @@ -6,10 +6,9 @@ import ( "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" - "github.com/smartcontractkit/cre-cli/internal/runtime" ) -func New(runtimeContext *runtime.Context) *cobra.Command { +func New() *cobra.Command { limitsCmd := &cobra.Command{ Use: "limits", Short: "Manage simulation limits", diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index 96ea33c4..f03a7bdf 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -32,7 +32,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { workflowCmd.AddCommand(deploy.New(runtimeContext)) workflowCmd.AddCommand(hash.New(runtimeContext)) workflowCmd.AddCommand(simulate.New(runtimeContext)) - workflowCmd.AddCommand(limits.New(runtimeContext)) + workflowCmd.AddCommand(limits.New()) return workflowCmd } From ef5c9bcb9d8152e6a3045d9c694807fbed70f51a Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 19 Mar 2026 12:18:58 -0300 Subject: [PATCH 09/12] unit tests --- .../simulate/limited_capabilities_test.go | 441 ++++++++++++++++++ cmd/workflow/simulate/limits_test.go | 179 +++++++ 2 files changed, 620 insertions(+) create mode 100644 cmd/workflow/simulate/limited_capabilities_test.go create mode 100644 cmd/workflow/simulate/limits_test.go diff --git a/cmd/workflow/simulate/limited_capabilities_test.go b/cmd/workflow/simulate/limited_capabilities_test.go new file mode 100644 index 00000000..9a8a0016 --- /dev/null +++ b/cmd/workflow/simulate/limited_capabilities_test.go @@ -0,0 +1,441 @@ +package simulate + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + caperrors "github.com/smartcontractkit/chainlink-common/pkg/capabilities/errors" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/confidentialhttp" + customhttp "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/actions/http" + evmcappb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + valuespb "github.com/smartcontractkit/chainlink-protos/cre/go/values/pb" +) + +type capabilityBaseStub struct{} + +func (capabilityBaseStub) Start(context.Context) error { return nil } +func (capabilityBaseStub) Close() error { return nil } +func (capabilityBaseStub) HealthReport() map[string]error { + return map[string]error{} +} +func (capabilityBaseStub) Name() string { return "stub" } +func (capabilityBaseStub) Description() string { return "stub" } +func (capabilityBaseStub) Ready() error { return nil } +func (capabilityBaseStub) Initialise(context.Context, core.StandardCapabilitiesDependencies) error { + return nil +} + +type httpClientCapabilityStub struct { + capabilityBaseStub + sendRequestFn func(context.Context, commonCap.RequestMetadata, *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) + sendRequestCalls int +} + +func (s *httpClientCapabilityStub) SendRequest(ctx context.Context, metadata commonCap.RequestMetadata, input *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) { + s.sendRequestCalls++ + if s.sendRequestFn != nil { + return s.sendRequestFn(ctx, metadata, input) + } + return nil, nil +} + +type confidentialHTTPClientCapabilityStub struct { + capabilityBaseStub + sendRequestFn func(context.Context, commonCap.RequestMetadata, *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) + sendRequestCalls int +} + +func (s *confidentialHTTPClientCapabilityStub) SendRequest(ctx context.Context, metadata commonCap.RequestMetadata, input *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) { + s.sendRequestCalls++ + if s.sendRequestFn != nil { + return s.sendRequestFn(ctx, metadata, input) + } + return nil, nil +} + +type consensusCapabilityStub struct { + capabilityBaseStub + simpleFn func(context.Context, commonCap.RequestMetadata, *sdkpb.SimpleConsensusInputs) (*commonCap.ResponseAndMetadata[*valuespb.Value], caperrors.Error) + reportFn func(context.Context, commonCap.RequestMetadata, *sdkpb.ReportRequest) (*commonCap.ResponseAndMetadata[*sdkpb.ReportResponse], caperrors.Error) + simpleCalls int + reportCalls int +} + +func (s *consensusCapabilityStub) Simple(ctx context.Context, metadata commonCap.RequestMetadata, input *sdkpb.SimpleConsensusInputs) (*commonCap.ResponseAndMetadata[*valuespb.Value], caperrors.Error) { + s.simpleCalls++ + if s.simpleFn != nil { + return s.simpleFn(ctx, metadata, input) + } + return nil, nil +} + +func (s *consensusCapabilityStub) Report(ctx context.Context, metadata commonCap.RequestMetadata, input *sdkpb.ReportRequest) (*commonCap.ResponseAndMetadata[*sdkpb.ReportResponse], caperrors.Error) { + s.reportCalls++ + if s.reportFn != nil { + return s.reportFn(ctx, metadata, input) + } + return nil, nil +} + +type evmClientCapabilityStub struct { + capabilityBaseStub + writeReportFn func(context.Context, commonCap.RequestMetadata, *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) + writeReportCalls int +} + +func (s *evmClientCapabilityStub) CallContract(context.Context, commonCap.RequestMetadata, *evmcappb.CallContractRequest) (*commonCap.ResponseAndMetadata[*evmcappb.CallContractReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) FilterLogs(context.Context, commonCap.RequestMetadata, *evmcappb.FilterLogsRequest) (*commonCap.ResponseAndMetadata[*evmcappb.FilterLogsReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) BalanceAt(context.Context, commonCap.RequestMetadata, *evmcappb.BalanceAtRequest) (*commonCap.ResponseAndMetadata[*evmcappb.BalanceAtReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) EstimateGas(context.Context, commonCap.RequestMetadata, *evmcappb.EstimateGasRequest) (*commonCap.ResponseAndMetadata[*evmcappb.EstimateGasReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) GetTransactionByHash(context.Context, commonCap.RequestMetadata, *evmcappb.GetTransactionByHashRequest) (*commonCap.ResponseAndMetadata[*evmcappb.GetTransactionByHashReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) GetTransactionReceipt(context.Context, commonCap.RequestMetadata, *evmcappb.GetTransactionReceiptRequest) (*commonCap.ResponseAndMetadata[*evmcappb.GetTransactionReceiptReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) HeaderByNumber(context.Context, commonCap.RequestMetadata, *evmcappb.HeaderByNumberRequest) (*commonCap.ResponseAndMetadata[*evmcappb.HeaderByNumberReply], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) RegisterLogTrigger(context.Context, string, commonCap.RequestMetadata, *evmcappb.FilterLogTriggerRequest) (<-chan commonCap.TriggerAndId[*evmcappb.Log], caperrors.Error) { + return nil, nil +} + +func (s *evmClientCapabilityStub) UnregisterLogTrigger(context.Context, string, commonCap.RequestMetadata, *evmcappb.FilterLogTriggerRequest) caperrors.Error { + return nil +} + +func (s *evmClientCapabilityStub) WriteReport(ctx context.Context, metadata commonCap.RequestMetadata, input *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) { + s.writeReportCalls++ + if s.writeReportFn != nil { + return s.writeReportFn(ctx, metadata, input) + } + return nil, nil +} + +func (s *evmClientCapabilityStub) AckEvent(context.Context, string, string, string) caperrors.Error { + return nil +} + +func (s *evmClientCapabilityStub) ChainSelector() uint64 { return 0 } + +func newTestLimits(t *testing.T) *SimulationLimits { + t.Helper() + limits, err := DefaultLimits() + require.NoError(t, err) + return limits +} + +func TestLimitedHTTPActionRejectsOversizedRequest(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.HTTPAction.RequestSizeLimit.DefaultValue = 4 + + inner := &httpClientCapabilityStub{} + wrapper := NewLimitedHTTPAction(inner, limits) + + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{Body: []byte("12345")}) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "HTTP request body size 5 bytes exceeds limit of 4 bytes") + assert.Equal(t, 0, inner.sendRequestCalls) +} + +func TestLimitedHTTPActionAppliesTimeoutAndAllowsBoundarySizedPayloads(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.HTTPAction.RequestSizeLimit.DefaultValue = 4 + limits.Workflows.HTTPAction.ResponseSizeLimit.DefaultValue = 5 + limits.Workflows.HTTPAction.ConnectionTimeout.DefaultValue = 2 * time.Second + + inner := &httpClientCapabilityStub{ + sendRequestFn: func(ctx context.Context, _ commonCap.RequestMetadata, input *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) { + deadline, ok := ctx.Deadline() + require.True(t, ok) + remaining := time.Until(deadline) + assert.LessOrEqual(t, remaining, 2*time.Second) + assert.Greater(t, remaining, time.Second) + assert.Equal(t, []byte("1234"), input.GetBody()) + return &commonCap.ResponseAndMetadata[*customhttp.Response]{ + Response: &customhttp.Response{Body: []byte("12345")}, + }, nil + }, + } + + wrapper := NewLimitedHTTPAction(inner, limits) + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{Body: []byte("1234")}) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, []byte("12345"), resp.Response.GetBody()) + assert.Equal(t, 1, inner.sendRequestCalls) +} + +func TestLimitedHTTPActionRejectsOversizedResponse(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.HTTPAction.ResponseSizeLimit.DefaultValue = 3 + + inner := &httpClientCapabilityStub{ + sendRequestFn: func(context.Context, commonCap.RequestMetadata, *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) { + return &commonCap.ResponseAndMetadata[*customhttp.Response]{ + Response: &customhttp.Response{Body: []byte("1234")}, + }, nil + }, + } + + wrapper := NewLimitedHTTPAction(inner, limits) + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{}) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "HTTP response body size 4 bytes exceeds limit of 3 bytes") + assert.Equal(t, 1, inner.sendRequestCalls) +} + +func TestLimitedHTTPActionPassesThroughInnerError(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + expectedResp := &commonCap.ResponseAndMetadata[*customhttp.Response]{Response: &customhttp.Response{Body: []byte("ok")}} + expectedErr := caperrors.NewPublicUserError(errors.New("boom"), caperrors.ResourceExhausted) + + inner := &httpClientCapabilityStub{ + sendRequestFn: func(context.Context, commonCap.RequestMetadata, *customhttp.Request) (*commonCap.ResponseAndMetadata[*customhttp.Response], caperrors.Error) { + return expectedResp, expectedErr + }, + } + + wrapper := NewLimitedHTTPAction(inner, limits) + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &customhttp.Request{}) + require.Error(t, err) + assert.Same(t, expectedResp, resp) + assert.True(t, expectedErr.Equals(err)) + assert.Equal(t, 1, inner.sendRequestCalls) +} + +func TestLimitedConfidentialHTTPActionRejectsOversizedRequest(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ConfidentialHTTP.RequestSizeLimit.DefaultValue = 4 + + inner := &confidentialHTTPClientCapabilityStub{} + wrapper := NewLimitedConfidentialHTTPAction(inner, limits) + + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &confidentialhttp.ConfidentialHTTPRequest{ + Request: &confidentialhttp.HTTPRequest{Body: &confidentialhttp.HTTPRequest_BodyString{BodyString: "12345"}}, + }) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "confidential HTTP request body size 5 bytes exceeds limit of 4 bytes") + assert.Equal(t, 0, inner.sendRequestCalls) +} + +func TestLimitedConfidentialHTTPActionAppliesTimeoutAndAllowsBoundarySizedPayloads(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ConfidentialHTTP.RequestSizeLimit.DefaultValue = 4 + limits.Workflows.ConfidentialHTTP.ResponseSizeLimit.DefaultValue = 5 + limits.Workflows.ConfidentialHTTP.ConnectionTimeout.DefaultValue = 2 * time.Second + + inner := &confidentialHTTPClientCapabilityStub{ + sendRequestFn: func(ctx context.Context, _ commonCap.RequestMetadata, input *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) { + deadline, ok := ctx.Deadline() + require.True(t, ok) + remaining := time.Until(deadline) + assert.LessOrEqual(t, remaining, 2*time.Second) + assert.Greater(t, remaining, time.Second) + assert.Equal(t, []byte("1234"), input.GetRequest().GetBodyBytes()) + return &commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse]{ + Response: &confidentialhttp.HTTPResponse{Body: []byte("12345")}, + }, nil + }, + } + + wrapper := NewLimitedConfidentialHTTPAction(inner, limits) + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &confidentialhttp.ConfidentialHTTPRequest{ + Request: &confidentialhttp.HTTPRequest{Body: &confidentialhttp.HTTPRequest_BodyBytes{BodyBytes: []byte("1234")}}, + }) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, []byte("12345"), resp.Response.GetBody()) + assert.Equal(t, 1, inner.sendRequestCalls) +} + +func TestLimitedConfidentialHTTPActionRejectsOversizedResponse(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ConfidentialHTTP.ResponseSizeLimit.DefaultValue = 3 + + inner := &confidentialHTTPClientCapabilityStub{ + sendRequestFn: func(context.Context, commonCap.RequestMetadata, *confidentialhttp.ConfidentialHTTPRequest) (*commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse], caperrors.Error) { + return &commonCap.ResponseAndMetadata[*confidentialhttp.HTTPResponse]{ + Response: &confidentialhttp.HTTPResponse{Body: []byte("1234")}, + }, nil + }, + } + + wrapper := NewLimitedConfidentialHTTPAction(inner, limits) + resp, err := wrapper.SendRequest(context.Background(), commonCap.RequestMetadata{}, &confidentialhttp.ConfidentialHTTPRequest{}) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "confidential HTTP response body size 4 bytes exceeds limit of 3 bytes") + assert.Equal(t, 1, inner.sendRequestCalls) +} + +func TestLimitedConsensusNoDAGSimpleRejectsOversizedObservation(t *testing.T) { + t.Parallel() + + input := &sdkpb.SimpleConsensusInputs{ + Observation: &sdkpb.SimpleConsensusInputs_Error{Error: strings.Repeat("x", 64)}, + } + + limits := newTestLimits(t) + limits.Workflows.Consensus.ObservationSizeLimit.DefaultValue = config.Size(proto.Size(input) - 1) + + inner := &consensusCapabilityStub{} + wrapper := NewLimitedConsensusNoDAG(inner, limits) + + resp, err := wrapper.Simple(context.Background(), commonCap.RequestMetadata{}, input) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "consensus observation size") + assert.Equal(t, 0, inner.simpleCalls) +} + +func TestLimitedConsensusNoDAGSimpleDelegatesWhenWithinLimit(t *testing.T) { + t.Parallel() + + input := &sdkpb.SimpleConsensusInputs{ + Observation: &sdkpb.SimpleConsensusInputs_Error{Error: "ok"}, + } + + limits := newTestLimits(t) + limits.Workflows.Consensus.ObservationSizeLimit.DefaultValue = config.Size(proto.Size(input)) + expectedResp := &commonCap.ResponseAndMetadata[*valuespb.Value]{Response: &valuespb.Value{}} + + inner := &consensusCapabilityStub{ + simpleFn: func(_ context.Context, _ commonCap.RequestMetadata, got *sdkpb.SimpleConsensusInputs) (*commonCap.ResponseAndMetadata[*valuespb.Value], caperrors.Error) { + assert.Same(t, input, got) + return expectedResp, nil + }, + } + + wrapper := NewLimitedConsensusNoDAG(inner, limits) + resp, err := wrapper.Simple(context.Background(), commonCap.RequestMetadata{}, input) + require.NoError(t, err) + assert.Same(t, expectedResp, resp) + assert.Equal(t, 1, inner.simpleCalls) +} + +func TestLimitedConsensusNoDAGReportDelegates(t *testing.T) { + t.Parallel() + + input := &sdkpb.ReportRequest{EncodedPayload: []byte("payload")} + expectedResp := &commonCap.ResponseAndMetadata[*sdkpb.ReportResponse]{Response: &sdkpb.ReportResponse{RawReport: []byte("report")}} + + inner := &consensusCapabilityStub{ + reportFn: func(_ context.Context, _ commonCap.RequestMetadata, got *sdkpb.ReportRequest) (*commonCap.ResponseAndMetadata[*sdkpb.ReportResponse], caperrors.Error) { + assert.Same(t, input, got) + return expectedResp, nil + }, + } + + wrapper := NewLimitedConsensusNoDAG(inner, newTestLimits(t)) + resp, err := wrapper.Report(context.Background(), commonCap.RequestMetadata{}, input) + require.NoError(t, err) + assert.Same(t, expectedResp, resp) + assert.Equal(t, 1, inner.reportCalls) +} + +func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ChainWrite.ReportSizeLimit.DefaultValue = 4 + + inner := &evmClientCapabilityStub{} + wrapper := NewLimitedEVMChain(inner, limits) + + resp, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{ + Report: &sdkpb.ReportResponse{RawReport: []byte("12345")}, + }) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "chain write report size 5 bytes exceeds limit of 4 bytes") + assert.Equal(t, 0, inner.writeReportCalls) +} + +func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ChainWrite.EVM.GasLimit.Default.DefaultValue = 10 + + inner := &evmClientCapabilityStub{} + wrapper := NewLimitedEVMChain(inner, limits) + + resp, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{ + GasConfig: &evmcappb.GasConfig{GasLimit: 11}, + }) + require.Error(t, err) + assert.Nil(t, resp) + assert.Contains(t, err.Error(), "EVM gas limit 11 exceeds maximum of 10") + assert.Equal(t, 0, inner.writeReportCalls) +} + +func TestLimitedEVMChainWriteReportDelegatesOnBoundaryValues(t *testing.T) { + t.Parallel() + + limits := newTestLimits(t) + limits.Workflows.ChainWrite.ReportSizeLimit.DefaultValue = 4 + limits.Workflows.ChainWrite.EVM.GasLimit.Default.DefaultValue = 10 + + input := &evmcappb.WriteReportRequest{ + Report: &sdkpb.ReportResponse{RawReport: []byte("1234")}, + GasConfig: &evmcappb.GasConfig{GasLimit: 10}, + } + expectedResp := &commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply]{Response: &evmcappb.WriteReportReply{}} + + inner := &evmClientCapabilityStub{ + writeReportFn: func(_ context.Context, _ commonCap.RequestMetadata, got *evmcappb.WriteReportRequest) (*commonCap.ResponseAndMetadata[*evmcappb.WriteReportReply], caperrors.Error) { + assert.Same(t, input, got) + return expectedResp, nil + }, + } + + wrapper := NewLimitedEVMChain(inner, limits) + resp, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, input) + require.NoError(t, err) + assert.Same(t, expectedResp, resp) + assert.Equal(t, 1, inner.writeReportCalls) +} diff --git a/cmd/workflow/simulate/limits_test.go b/cmd/workflow/simulate/limits_test.go new file mode 100644 index 00000000..02f01ea2 --- /dev/null +++ b/cmd/workflow/simulate/limits_test.go @@ -0,0 +1,179 @@ +package simulate + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" +) + +func writeLimitsFile(t *testing.T, contents string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "limits.json") + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + return path +} + +func TestDefaultLimitsAndExportDefaultLimitsJSON(t *testing.T) { + t.Parallel() + + limits, err := DefaultLimits() + require.NoError(t, err) + + assert.Equal(t, 10_000, limits.HTTPRequestSizeLimit()) + assert.Equal(t, 100_000, limits.HTTPResponseSizeLimit()) + assert.Equal(t, 10_000, limits.ConfHTTPRequestSizeLimit()) + assert.Equal(t, 100_000, limits.ConfHTTPResponseSizeLimit()) + assert.Equal(t, 100_000, limits.ConsensusObservationSizeLimit()) + assert.Equal(t, 5_000, limits.ChainWriteReportSizeLimit()) + assert.Equal(t, uint64(5_000_000), limits.ChainWriteEVMGasLimit()) + assert.Equal(t, 100_000_000, limits.WASMBinarySize()) + assert.Equal(t, 20_000_000, limits.WASMCompressedBinarySize()) + assert.JSONEq(t, string(defaultLimitsJSON), string(ExportDefaultLimitsJSON())) +} + +func TestLoadLimitsParsesCustomFileAndPreservesDefaultsForUnsetFields(t *testing.T) { + t.Parallel() + + path := writeLimitsFile(t, `{ + "HTTPAction": { + "RequestSizeLimit": "7kb", + "ConnectionTimeout": "2s" + }, + "ChainWrite": { + "ReportSizeLimit": "9kb", + "EVM": { + "GasLimit": { + "Default": "123" + } + } + } + }`) + + limits, err := LoadLimits(path) + require.NoError(t, err) + + assert.Equal(t, 7_000, limits.HTTPRequestSizeLimit()) + assert.Equal(t, 100_000, limits.HTTPResponseSizeLimit(), "unset values should keep embedded defaults") + assert.Equal(t, 9_000, limits.ChainWriteReportSizeLimit()) + assert.Equal(t, uint64(123), limits.ChainWriteEVMGasLimit()) + assert.Equal(t, 2*time.Second, limits.Workflows.HTTPAction.ConnectionTimeout.DefaultValue) +} + +func TestLoadLimitsReturnsHelpfulErrors(t *testing.T) { + t.Parallel() + + t.Run("missing file", func(t *testing.T) { + _, err := LoadLimits(filepath.Join(t.TempDir(), "missing.json")) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to read limits file") + }) + + t.Run("invalid json", func(t *testing.T) { + path := writeLimitsFile(t, `{invalid json`) + _, err := LoadLimits(path) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse limits JSON") + }) +} + +func TestResolveLimitsHandlesAllSupportedModes(t *testing.T) { + t.Parallel() + + for _, flag := range []string{"", "none"} { + limits, err := ResolveLimits(flag) + require.NoError(t, err) + assert.Nil(t, limits) + } + + defaultLimits, err := ResolveLimits("default") + require.NoError(t, err) + baseline, err := DefaultLimits() + require.NoError(t, err) + assert.Equal(t, baseline.HTTPRequestSizeLimit(), defaultLimits.HTTPRequestSizeLimit()) + assert.Equal(t, baseline.ChainWriteEVMGasLimit(), defaultLimits.ChainWriteEVMGasLimit()) + + path := writeLimitsFile(t, `{"Consensus":{"ObservationSizeLimit":"2kb"}}`) + customLimits, err := ResolveLimits(path) + require.NoError(t, err) + assert.Equal(t, 2_000, customLimits.ConsensusObservationSizeLimit()) +} + +func TestApplyEngineLimitsCopiesSupportedFieldsAndPreservesChainAllowed(t *testing.T) { + t.Parallel() + + cfg := cresettings.Default.PerWorkflow + cfg.ChainAllowed.Default.DefaultValue = true + + limits := newTestLimits(t) + limits.Workflows.ExecutionTimeout.DefaultValue = 11 * time.Second + limits.Workflows.ExecutionResponseLimit.DefaultValue = 2048 + limits.Workflows.ExecutionConcurrencyLimit.DefaultValue = 9 + limits.Workflows.CapabilityConcurrencyLimit.DefaultValue = 7 + limits.Workflows.CapabilityCallTimeout.DefaultValue = 12 * time.Second + limits.Workflows.SecretsConcurrencyLimit.DefaultValue = 6 + limits.Workflows.TriggerRegistrationsTimeout.DefaultValue = 13 * time.Second + limits.Workflows.TriggerEventQueueLimit.DefaultValue = 14 + limits.Workflows.TriggerEventQueueTimeout.DefaultValue = 15 * time.Second + limits.Workflows.TriggerSubscriptionTimeout.DefaultValue = 16 * time.Second + limits.Workflows.TriggerSubscriptionLimit.DefaultValue = 17 + limits.Workflows.WASMMemoryLimit.DefaultValue = 4096 + limits.Workflows.WASMBinarySizeLimit.DefaultValue = 8192 + limits.Workflows.WASMCompressedBinarySizeLimit.DefaultValue = 1024 + limits.Workflows.WASMConfigSizeLimit.DefaultValue = 512 + limits.Workflows.WASMSecretsSizeLimit.DefaultValue = 256 + limits.Workflows.LogLineLimit.DefaultValue = 128 + limits.Workflows.LogEventLimit.DefaultValue = 25 + limits.Workflows.ChainRead.CallLimit.DefaultValue = 3 + limits.Workflows.ChainWrite.TargetsLimit.DefaultValue = 4 + limits.Workflows.Consensus.CallLimit.DefaultValue = 5 + limits.Workflows.HTTPAction.CallLimit.DefaultValue = 6 + limits.Workflows.ConfidentialHTTP.CallLimit.DefaultValue = 7 + limits.Workflows.Secrets.CallLimit.DefaultValue = 8 + limits.Workflows.CRONTrigger.FastestScheduleInterval.DefaultValue = 30 * time.Second + + applyEngineLimits(&cfg, limits) + + assert.Equal(t, 11*time.Second, cfg.ExecutionTimeout.DefaultValue) + assert.Equal(t, 2048, int(cfg.ExecutionResponseLimit.DefaultValue)) + assert.Equal(t, 9, cfg.ExecutionConcurrencyLimit.DefaultValue) + assert.Equal(t, 7, cfg.CapabilityConcurrencyLimit.DefaultValue) + assert.Equal(t, 12*time.Second, cfg.CapabilityCallTimeout.DefaultValue) + assert.Equal(t, 6, cfg.SecretsConcurrencyLimit.DefaultValue) + assert.Equal(t, 13*time.Second, cfg.TriggerRegistrationsTimeout.DefaultValue) + assert.Equal(t, 14, cfg.TriggerEventQueueLimit.DefaultValue) + assert.Equal(t, 15*time.Second, cfg.TriggerEventQueueTimeout.DefaultValue) + assert.Equal(t, 16*time.Second, cfg.TriggerSubscriptionTimeout.DefaultValue) + assert.Equal(t, 17, cfg.TriggerSubscriptionLimit.DefaultValue) + assert.Equal(t, 4096, int(cfg.WASMMemoryLimit.DefaultValue)) + assert.Equal(t, 8192, int(cfg.WASMBinarySizeLimit.DefaultValue)) + assert.Equal(t, 1024, int(cfg.WASMCompressedBinarySizeLimit.DefaultValue)) + assert.Equal(t, 512, int(cfg.WASMConfigSizeLimit.DefaultValue)) + assert.Equal(t, 256, int(cfg.WASMSecretsSizeLimit.DefaultValue)) + assert.Equal(t, 128, int(cfg.LogLineLimit.DefaultValue)) + assert.Equal(t, 25, cfg.LogEventLimit.DefaultValue) + assert.Equal(t, 3, cfg.ChainRead.CallLimit.DefaultValue) + assert.Equal(t, 4, cfg.ChainWrite.TargetsLimit.DefaultValue) + assert.Equal(t, 5, cfg.Consensus.CallLimit.DefaultValue) + assert.Equal(t, 6, cfg.HTTPAction.CallLimit.DefaultValue) + assert.Equal(t, 7, cfg.ConfidentialHTTP.CallLimit.DefaultValue) + assert.Equal(t, 8, cfg.Secrets.CallLimit.DefaultValue) + assert.Equal(t, 30*time.Second, cfg.CRONTrigger.FastestScheduleInterval.DefaultValue) + assert.True(t, cfg.ChainAllowed.Default.DefaultValue, "simulation should preserve allow-all ChainAllowed settings") +} + +func TestSimulationLimitsSummaryIncludesKeyLimitValues(t *testing.T) { + t.Parallel() + + summary := newTestLimits(t).LimitsSummary() + assert.Contains(t, summary, "HTTP: req=10kb resp=100kb timeout=10s") + assert.Contains(t, summary, "ConfHTTP: req=10kb resp=100kb timeout=10s") + assert.Contains(t, summary, "Consensus obs=100kb") + assert.Contains(t, summary, "ChainWrite report=5kb gas=5000000") + assert.Contains(t, summary, "WASM binary=100mb compressed=20mb") +} From afb9712279c9f57da5af3969a01b50086a5e5854 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 19 Mar 2026 12:22:48 -0300 Subject: [PATCH 10/12] fallback to default if flag is empty --- cmd/workflow/simulate/limits.go | 5 +++-- cmd/workflow/simulate/limits_test.go | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/workflow/simulate/limits.go b/cmd/workflow/simulate/limits.go index 5c146c87..4feffa84 100644 --- a/cmd/workflow/simulate/limits.go +++ b/cmd/workflow/simulate/limits.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" ) @@ -164,11 +165,11 @@ func ExportDefaultLimitsJSON() []byte { // ResolveLimits resolves a --limits flag value to SimulationLimits. // Returns nil if limitsFlag is "none" (no limits enforcement). func ResolveLimits(limitsFlag string) (*SimulationLimits, error) { - if limitsFlag == "" || limitsFlag == "none" { + if limitsFlag == "none" { return nil, nil } - if limitsFlag == "default" { + if strings.TrimSpace(limitsFlag) == "" || limitsFlag == "default" { return DefaultLimits() } diff --git a/cmd/workflow/simulate/limits_test.go b/cmd/workflow/simulate/limits_test.go index 02f01ea2..487adbf4 100644 --- a/cmd/workflow/simulate/limits_test.go +++ b/cmd/workflow/simulate/limits_test.go @@ -85,11 +85,10 @@ func TestLoadLimitsReturnsHelpfulErrors(t *testing.T) { func TestResolveLimitsHandlesAllSupportedModes(t *testing.T) { t.Parallel() - for _, flag := range []string{"", "none"} { - limits, err := ResolveLimits(flag) - require.NoError(t, err) - assert.Nil(t, limits) - } + flag := "none" + limits, err := ResolveLimits(flag) + require.NoError(t, err) + assert.Nil(t, limits) defaultLimits, err := ResolveLimits("default") require.NoError(t, err) From d265cc23cc17548add32b9ebeb84068899769d49 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 19 Mar 2026 13:02:31 -0300 Subject: [PATCH 11/12] fix apply order --- cmd/workflow/simulate/simulate.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index dc69e951..74826ff6 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -675,15 +675,15 @@ func run( }, }, WorkflowSettingsCfgFn: func(cfg *cresettings.Workflows) { - cfg.ChainAllowed = commonsettings.PerChainSelector( - commonsettings.Bool(true), // Allow all chains in simulation - map[string]bool{}, - ) // Apply simulation limits to engine-level settings when --limits is set if simLimits != nil { applyEngineLimits(cfg, simLimits) - // Re-apply allow-all chains since applyEngineLimits does not touch ChainAllowed } + // Always allow all chains in simulation, overriding any chain restrictions from limits + cfg.ChainAllowed = commonsettings.PerChainSelector( + commonsettings.Bool(true), + map[string]bool{}, + ) }, }) From 86b095481ba6b96128519e0a261a28abdf971552 Mon Sep 17 00:00:00 2001 From: Tarcisio Ferraz Date: Thu, 19 Mar 2026 13:03:00 -0300 Subject: [PATCH 12/12] use brotli compression --- cmd/workflow/simulate/simulate.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 74826ff6..771eef49 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -1,8 +1,6 @@ package simulate import ( - "bytes" - "compress/gzip" "context" "crypto/ecdsa" "encoding/json" @@ -357,17 +355,12 @@ func (h *handler) Execute(inputs Inputs) error { compressedLimit := simLimits.WASMCompressedBinarySize() if compressedLimit > 0 { - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - if _, err := gz.Write(wasmFileBinary); err != nil { - return fmt.Errorf("failed to compress WASM binary for size check: %w", err) - } - if err := gz.Close(); err != nil { - return fmt.Errorf("failed to finalize WASM compression: %w", err) + compressed, err := cmdcommon.CompressBrotli(wasmFileBinary) + if err != nil { + return fmt.Errorf("failed to compress brotli: %w", err) } - compressedSize := buf.Len() - if compressedSize > compressedLimit { - return fmt.Errorf("WASM compressed binary size %d bytes exceeds limit of %d bytes", compressedSize, compressedLimit) + if len(compressed) > compressedLimit { + return fmt.Errorf("WASM compressed binary size %d bytes exceeds limit of %d bytes", len(compressed), compressedLimit) } }