Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,13 @@ type Handler struct {
func NewHandler(execCtx context.Context, ctx *runtime.Context, secretsFilePath, secretsAuth string) (*Handler, error) {
var pk *ecdsa.PrivateKey
var err error
if ctx.Settings.User.EthPrivateKey.IsSet() {
pk, err = crypto.HexToECDSA(ctx.Settings.User.EthPrivateKey.Hex())
if ethKey := ctx.Settings.User.PrivateKey(settings.EVM); ethKey != "" {
pk, err = crypto.HexToECDSA(ethKey)
if err != nil {
return nil, fmt.Errorf("failed to decode the provided private key: %w", err)
}
} else {
ctx.Logger.Debug().Msg("No EthPrivateKey found in settings; assuming a multisig request.")

ctx.Logger.Debug().Msg("No EVM private key found in settings; assuming a multisig request.")
}

h := &Handler{
Expand Down
4 changes: 2 additions & 2 deletions cmd/secrets/common/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ func TestNewHandler_WorkflowRegistryClient(t *testing.T) {
Logger: &logger,
ClientFactory: cf,
Settings: &settings.Settings{
User: settings.UserSettings{EthPrivateKey: ""},
User: settings.UserSettings{PrivateKeys: map[string]string{settings.EVM.Name: ""}},
Workflow: settings.WorkflowSettings{},
},
EnvironmentSet: &environments.EnvironmentSet{GatewayURL: "http://localhost"},
Expand Down Expand Up @@ -446,7 +446,7 @@ func TestNewHandler_GatewayURL(t *testing.T) {
Logger: &logger,
ClientFactory: cf,
Settings: &settings.Settings{
User: settings.UserSettings{EthPrivateKey: ""},
User: settings.UserSettings{PrivateKeys: map[string]string{settings.EVM.Name: ""}},
Workflow: settings.WorkflowSettings{},
},
EnvironmentSet: &environments.EnvironmentSet{GatewayURL: "https://embedded.example.com/"},
Expand Down
6 changes: 3 additions & 3 deletions cmd/workflow/activate/activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand All @@ -48,7 +48,7 @@ func TestNonInteractive_WithYes_PassesGuard(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down Expand Up @@ -163,7 +163,7 @@ func TestWorkflowActivateCommand(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down
6 changes: 3 additions & 3 deletions cmd/workflow/delete/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand All @@ -47,7 +47,7 @@ func TestNonInteractive_WithYes_Proceeds(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down Expand Up @@ -134,7 +134,7 @@ func TestWorkflowDeleteCommand(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down
2 changes: 1 addition & 1 deletion cmd/workflow/hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command {
WorkflowName: s.Workflow.UserWorkflowSettings.WorkflowName,
WorkflowPath: s.Workflow.WorkflowArtifactSettings.WorkflowPath,
OwnerFromSettings: s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress,
PrivateKey: s.User.EthPrivateKey.Hex(),
PrivateKey: s.User.PrivateKey(settings.EVM),
SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag),
RegistryType: registryType,
DerivedOwner: runtimeContext.DerivedWorkflowOwner,
Expand Down
6 changes: 3 additions & 3 deletions cmd/workflow/pause/pause_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestNonInteractive_WithoutYes_ReturnsError(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand All @@ -47,7 +47,7 @@ func TestNonInteractive_WithYes_PassesGuard(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down Expand Up @@ -141,7 +141,7 @@ func TestWorkflowPauseCommand(t *testing.T) {
ctx := simulatedEnvironment.NewRuntimeContext()
ctx.Settings = &settings.Settings{
User: settings.UserSettings{
EthPrivateKey: chainsim.TestPrivateKey,
PrivateKeys: map[string]string{settings.EVM.Name: chainsim.TestPrivateKey},
},
}
ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA
Expand Down
10 changes: 4 additions & 6 deletions cmd/workflow/simulate/chain/evm/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink/v2/core/capabilities"
"github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes"

"github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain"
)

// EVMChainCapabilities holds the EVM chain capability servers created for simulation.
Expand All @@ -29,7 +31,7 @@ func NewEVMChainCapabilities(
forwarders map[uint64]string,
privateKey *ecdsa.PrivateKey,
dryRunChainWrite bool,
limits EVMChainLimits,
limits chain.Limits,
) (*EVMChainCapabilities, error) {
evmChains := make(map[uint64]*ManualEVMChain)
for sel, client := range clients {
Expand All @@ -48,11 +50,7 @@ func NewEVMChainCapabilities(
dryRunChainWrite,
)

// Wrap with limits enforcement if limits are provided
var evmCap evmserver.ClientCapability = evm
if limits != nil {
evmCap = NewLimitedEVMChain(evm, limits)
}
evmCap := NewLimitedEVMChain(evm, limits)

manualEVM := NewManualEVMChain(evmCap)
evmServer := evmserver.NewClientServer(manualEVM)
Expand Down
19 changes: 10 additions & 9 deletions cmd/workflow/simulate/chain/evm/chaintype.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ func (ct *EVMChainType) ResolveClients(v *viper.Viper) (chain.ResolvedChains, er
}

for _, ec := range expChains {
// Empty chain-type falls back to this chain type
if ec.ChainType != "" && !strings.EqualFold(ec.ChainType, ct.Name()) {
continue
}
if ec.ChainSelector == 0 {
return chain.ResolvedChains{}, fmt.Errorf("experimental chain missing chain-selector")
}
Expand Down Expand Up @@ -156,13 +160,9 @@ func (ct *EVMChainType) RegisterCapabilities(ctx context.Context, cfg chain.Capa
// cfg.Limits is the generic chain.Limits contract. The EVM chain type
// needs the wider EVMChainLimits contract (adds ChainWriteGasLimit). A
// nil cfg.Limits disables enforcement entirely.
var evmLimits EVMChainLimits
var evmLimits chain.Limits
if cfg.Limits != nil {
el, ok := cfg.Limits.(EVMChainLimits)
if !ok {
return nil, fmt.Errorf("EVM chain type: limits value does not implement evm.EVMChainLimits (got %T)", cfg.Limits)
}
evmLimits = el
evmLimits = ExtractLimits(cfg.Limits)
}

evmCaps, err := NewEVMChainCapabilities(
Expand Down Expand Up @@ -247,12 +247,13 @@ func (ct *EVMChainType) RunHealthCheck(resolved chain.ResolvedChains) error {
// is true, an invalid or default-sentinel key is a hard error. Otherwise a
// sentinel key is used with a warning so non-broadcast simulations can run.
func (ct *EVMChainType) ResolveKey(creSettings *settings.Settings, broadcast bool) (interface{}, error) {
pk, err := crypto.HexToECDSA(creSettings.User.EthPrivateKey.Hex())
pk, err := crypto.HexToECDSA(creSettings.User.PrivateKey(settings.EVM))
if err != nil {
// If the user explicitly set a key that looks like a hex string but is
// malformed (wrong length, invalid chars), always error with guidance.
// Skip placeholder values like DefaultEthPrivateKeyEnvPlaceholder from the default .env template.
if creSettings.User.EthPrivateKey.IsSet() && isHexString(creSettings.User.EthPrivateKey.Hex()) {
evmKey := creSettings.User.PrivateKey(settings.EVM)
if evmKey != "" && isHexString(evmKey) {
return nil, fmt.Errorf(
"invalid private key: expected 64 hex characters (256 bits), got %d characters.\n\n"+
"The CLI reads CRE_ETH_PRIVATE_KEY from your .env file or system environment.\n"+
Expand All @@ -261,7 +262,7 @@ func (ct *EVMChainType) ResolveKey(creSettings *settings.Settings, broadcast boo
" • Pasted an Ethereum address (40 chars) instead of a private key (64 chars)\n"+
" • Value has extra quotes — use CRE_ETH_PRIVATE_KEY=abc123... without wrapping quotes\n"+
" • Key was truncated during copy-paste",
len(creSettings.User.EthPrivateKey.Hex()))
len(creSettings.User.PrivateKey(settings.EVM)))
}
if broadcast {
return nil, fmt.Errorf(
Expand Down
15 changes: 14 additions & 1 deletion cmd/workflow/simulate/chain/evm/chaintype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func TestEVMChainType_ResolveKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ct := newEVMChainType()
s := &settings.Settings{User: settings.UserSettings{EthPrivateKey: settings.EthPrivateKeyHex(tt.pk)}}
s := &settings.Settings{User: settings.UserSettings{PrivateKeys: map[string]string{settings.EVM.Name: tt.pk}}}

var got interface{}
var err error
Expand Down Expand Up @@ -369,3 +369,16 @@ func TestEVMChainType_CollectCLIInputs_DefaultsOnly(t *testing.T) {
result := ct.CollectCLIInputs(v)
assert.Empty(t, result)
}

func TestEVMChainType_RegisterCapabilities_WrongPrivateKeyType(t *testing.T) {
t.Parallel()
ct := newEVMChainType()
cfg := chain.CapabilityConfig{
Clients: map[uint64]chain.ChainClient{},
Forwarders: map[uint64]string{},
PrivateKey: "not-an-ecdsa-key",
}
_, err := ct.RegisterCapabilities(context.Background(), cfg)
require.Error(t, err)
assert.Contains(t, err.Error(), "private key is not *ecdsa.PrivateKey")
}
24 changes: 6 additions & 18 deletions cmd/workflow/simulate/chain/evm/limited_capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,30 @@ import (
"github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain"
)

// EVMChainLimits is the EVM-scoped limit contract LimitedEVMChain enforces.
// It extends chain.Limits with EVM-specific accessors (e.g. gas limit) so
// non-EVM chain types cannot accidentally depend on EVM semantics.
type EVMChainLimits interface {
chain.Limits
ChainWriteGasLimit() uint64
}

// LimitedEVMChain wraps an evmserver.ClientCapability and enforces chain write
// report size and gas limits.
type LimitedEVMChain struct {
inner evmserver.ClientCapability
limits EVMChainLimits
limits chain.Limits
}

var _ evmserver.ClientCapability = (*LimitedEVMChain)(nil)

func NewLimitedEVMChain(inner evmserver.ClientCapability, limits EVMChainLimits) *LimitedEVMChain {
func NewLimitedEVMChain(inner evmserver.ClientCapability, limits chain.Limits) *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 {
if l.limits.ReportSize > 0 && input.Report != nil && len(input.Report.RawReport) > l.limits.ReportSize {
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),
fmt.Errorf("simulation limit exceeded: chain write report size %d bytes exceeds limit of %d bytes", len(input.Report.RawReport), l.limits.ReportSize),
caperrors.ResourceExhausted,
)
}

// Check gas limit
gasLimit := l.limits.ChainWriteGasLimit()
if gasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > gasLimit {
if l.limits.GasLimit > 0 && input.GasConfig != nil && input.GasConfig.GasLimit > l.limits.GasLimit {
return nil, caperrors.NewPublicUserError(
fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, gasLimit),
fmt.Errorf("simulation limit exceeded: EVM gas limit %d exceeds maximum of %d", input.GasConfig.GasLimit, l.limits.GasLimit),
caperrors.ResourceExhausted,
)
}
Expand Down
56 changes: 45 additions & 11 deletions cmd/workflow/simulate/chain/evm/limited_capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,9 @@ import (
evmserver "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/chain-capabilities/evm/server"
"github.com/smartcontractkit/chainlink-common/pkg/types/core"
sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk"
)

type stubEVMLimits struct {
reportSizeLimit int
gasLimit uint64
}

func (s *stubEVMLimits) ChainWriteReportSizeLimit() int { return s.reportSizeLimit }
func (s *stubEVMLimits) ChainWriteGasLimit() uint64 { return s.gasLimit }
"github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain"
)

type evmCapabilityBaseStub struct{}

Expand Down Expand Up @@ -94,7 +88,7 @@ func (s *evmClientCapabilityStub) ChainSelector() uint64 { return 0 }
func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) {
t.Parallel()

limits := &stubEVMLimits{reportSizeLimit: 4}
limits := chain.Limits{ReportSize: 4}
inner := &evmClientCapabilityStub{}
wrapper := NewLimitedEVMChain(inner, limits)

Expand All @@ -110,7 +104,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedReport(t *testing.T) {
func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) {
t.Parallel()

limits := &stubEVMLimits{gasLimit: 10}
limits := chain.Limits{GasLimit: 10}
inner := &evmClientCapabilityStub{}
wrapper := NewLimitedEVMChain(inner, limits)

Expand All @@ -126,7 +120,7 @@ func TestLimitedEVMChainWriteReportRejectsOversizedGasLimit(t *testing.T) {
func TestLimitedEVMChainWriteReportDelegatesOnBoundaryValues(t *testing.T) {
t.Parallel()

limits := &stubEVMLimits{reportSizeLimit: 4, gasLimit: 10}
limits := chain.Limits{ReportSize: 4, GasLimit: 10}

input := &evmcappb.WriteReportRequest{
Report: &sdkpb.ReportResponse{RawReport: []byte("1234")},
Expand All @@ -147,3 +141,43 @@ func TestLimitedEVMChainWriteReportDelegatesOnBoundaryValues(t *testing.T) {
assert.Same(t, expectedResp, resp)
assert.Equal(t, 1, inner.writeReportCalls)
}

func TestLimitedEVMChainWriteReportZeroLimitsDelegate(t *testing.T) {
t.Parallel()

inner := &evmClientCapabilityStub{}
wrapper := NewLimitedEVMChain(inner, chain.Limits{})

_, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{
Report: &sdkpb.ReportResponse{RawReport: make([]byte, 1_000_000)},
GasConfig: &evmcappb.GasConfig{GasLimit: 1_000_000_000},
})
require.NoError(t, err)
assert.Equal(t, 1, inner.writeReportCalls)
}

func TestLimitedEVMChainWriteReportNilGasConfigDelegates(t *testing.T) {
t.Parallel()

inner := &evmClientCapabilityStub{}
wrapper := NewLimitedEVMChain(inner, chain.Limits{ReportSize: 100, GasLimit: 10})

_, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{
Report: &sdkpb.ReportResponse{RawReport: []byte("x")},
})
require.NoError(t, err)
assert.Equal(t, 1, inner.writeReportCalls)
}

func TestLimitedEVMChainWriteReportNilReportDelegates(t *testing.T) {
t.Parallel()

inner := &evmClientCapabilityStub{}
wrapper := NewLimitedEVMChain(inner, chain.Limits{ReportSize: 100, GasLimit: 100})

_, err := wrapper.WriteReport(context.Background(), commonCap.RequestMetadata{}, &evmcappb.WriteReportRequest{
GasConfig: &evmcappb.GasConfig{GasLimit: 50},
})
require.NoError(t, err)
assert.Equal(t, 1, inner.writeReportCalls)
}
14 changes: 14 additions & 0 deletions cmd/workflow/simulate/chain/evm/limits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package evm

import (
"github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings"

"github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain"
)

func ExtractLimits(w *cresettings.Workflows) chain.Limits {
return chain.Limits{
ReportSize: int(w.ChainWrite.EVM.ReportSizeLimit.DefaultValue),
GasLimit: w.ChainWrite.EVM.GasLimit.Default.DefaultValue,
}
}
Loading