From 723265564a4dad61b62d28e93da78d77e541844c Mon Sep 17 00:00:00 2001 From: Gustavo Gama Date: Wed, 19 Nov 2025 18:54:28 -0300 Subject: [PATCH 1/2] fix: add maxGasLimit parameter --- Makefile | 2 +- cmd/start.go | 10 +++- pkg/cli/config.go | 10 ++++ pkg/timelock/const_test.go | 1 + pkg/timelock/operations_evm.go | 33 +++++++++++++ pkg/timelock/retry.go | 1 - pkg/timelock/scheduler.go | 4 +- pkg/timelock/worker_evm.go | 8 +++- pkg/timelock/worker_evm_test.go | 11 +++-- tests/containers/geth.go | 13 ++--- tests/integration/evm/onchain_ops.go | 21 ++++++++ tests/integration/evm/timelock_test.go | 66 ++++++++++++++++++++++---- 12 files changed, 152 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 584545f..af64a36 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ clean: .PHONY: test test: @echo "\n\t$(C_GREEN)# Run test and generate new coverage.out$(C_END)" - go test -short -coverprofile=coverage.out -covermode=atomic -race ./... + CTF_CONFIGS=./config.toml go test -short -coverprofile=coverage.out -covermode=atomic -race ./... .PHONY: coverage coverage: diff --git a/cmd/start.go b/cmd/start.go index 70cb660..d4a3c84 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -25,7 +25,7 @@ func startCommand() *cobra.Command { nodeURL, privateKey, timelockAddress, callProxyAddress, chainFamily string fromBlock, pollPeriod, eventListenerPollPeriod int64 - eventListenerPollSize uint64 + eventListenerPollSize, maxGasLimit uint64 dryRun bool ) @@ -47,6 +47,7 @@ func startCommand() *cobra.Command { startCmd.Flags().StringVarP(&callProxyAddress, "call-proxy-address", "f", timelockConf.CallProxyAddress, "Address of the target CallProxyAddress contract") startCmd.Flags().StringVarP(&privateKey, "private-key", "k", timelockConf.PrivateKey, "Private key used to execute transactions") startCmd.Flags().Int64Var(&fromBlock, "from-block", timelockConf.FromBlock, "Start watching from this block") + startCmd.Flags().Uint64Var(&maxGasLimit, "max-gas-limit", timelockConf.MaxGasLimit, "Network's maximum gas limit") startCmd.Flags().Int64Var(&pollPeriod, "poll-period", timelockConf.PollPeriod, "Poll period in seconds") startCmd.Flags().Int64Var(&eventListenerPollPeriod, "event-listener-poll-period", timelockConf.EventListenerPollPeriod, "Event Listener poll period in seconds") startCmd.Flags().Uint64Var(&eventListenerPollSize, "event-listener-poll-size", timelockConf.EventListenerPollSize, "Number of entries to fetch when polling logs") @@ -110,6 +111,11 @@ func startTimelock(cmd *cobra.Command) { } } + maxGasLimit, err := cmd.Flags().GetUint64("max-gas-limit") + if err != nil { + slog.Fatalf("value of max-gas-limit not set: %s", err.Error()) + } + fromBlock, err := cmd.Flags().GetInt64("from-block") if err != nil { slog.Fatalf("value of from-block not set: %s", err.Error()) @@ -137,7 +143,7 @@ func startTimelock(cmd *cobra.Command) { if chainFamily == chain_selectors.FamilyEVM { tWorker, err := timelock.NewTimelockWorkerEVM(nodeURL, timelockAddress, callProxyAddress, privateKey, - big.NewInt(fromBlock), pollPeriod, eventListenerPollPeriod, eventListenerPollSize, dryRun, slog) + big.NewInt(fromBlock), maxGasLimit, pollPeriod, eventListenerPollPeriod, eventListenerPollSize, dryRun, slog) if err != nil { slog.Fatalf("error creating the timelock-worker: %s", err.Error()) } diff --git a/pkg/cli/config.go b/pkg/cli/config.go index 2b7d5fd..ea271e2 100644 --- a/pkg/cli/config.go +++ b/pkg/cli/config.go @@ -19,6 +19,7 @@ type Config struct { CallProxyAddress string `mapstructure:"CALL_PROXY_ADDRESS"` PrivateKey string `mapstructure:"PRIVATE_KEY"` FromBlock int64 `mapstructure:"FROM_BLOCK"` + MaxGasLimit uint64 `mapstructure:"MAX_GAS_LIMIT"` PollPeriod int64 `mapstructure:"POLL_PERIOD"` EventListenerPollPeriod int64 `mapstructure:"EVENT_LISTENER_POLL_PERIOD"` EventListenerPollSize uint64 `mapstructure:"EVENT_LISTENER_POLL_SIZE"` @@ -75,6 +76,15 @@ func NewTimelockCLI() (*Config, error) { c.FromBlock = int64(fb) } + if os.Getenv("MAX_GAS_LIMIT") != "" { + mgl, err := strconv.ParseUint(os.Getenv("MAX_GAS_LIMIT"), 10, 64) + if err != nil { + return nil, fmt.Errorf("unable to parse MAX_GAS_LIMIT value: %w", err) + } + + c.MaxGasLimit = mgl + } + if os.Getenv("POLL_PERIOD") != "" { pp, err := strconv.Atoi(os.Getenv("POLL_PERIOD")) if err != nil { diff --git a/pkg/timelock/const_test.go b/pkg/timelock/const_test.go index 913b055..0cabf8d 100644 --- a/pkg/timelock/const_test.go +++ b/pkg/timelock/const_test.go @@ -14,6 +14,7 @@ var ( testCallProxyAddress = "0x0000000000000000000000000000000000000000" testPrivateKey = "8064bf62c044d2654705b9d0cfbd666c2649fabb76ed8f4b9d8d3eb28267e3cf" testFromBlock = big.NewInt(0) + testMaxGasLimit = uint64(0) testPollPeriod = 5 testEventListenerPollPeriod = 1 testEventListenerPollSize = uint64(10) diff --git a/pkg/timelock/operations_evm.go b/pkg/timelock/operations_evm.go index 05a009e..f85a02b 100644 --- a/pkg/timelock/operations_evm.go +++ b/pkg/timelock/operations_evm.go @@ -3,6 +3,7 @@ package timelock import ( "context" "crypto/ecdsa" + "errors" "fmt" "math/big" @@ -15,6 +16,8 @@ import ( contracts "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers" ) +var ErrMaxGasLimit = errors.New("transaction gas exceeds max gas limit") + // execute runs the CallScheduled operation if: // - The predecessor operation is finished // - The operation is ready to be executed @@ -41,6 +44,9 @@ func (tw *WorkerEVM) execute(ctx context.Context, op []TimelockCallScheduled) { tx, err := tw.executeCallSchedule(ctx, &tw.executeContract.RBACTimelockTransactor, op, tw.privateKey) if err != nil || tx == nil { tw.logger.Errorf("execute operation %x error: %s", opId, err.Error()) + if errors.Is(err, ErrMaxGasLimit) { + tw.scheduler.delFromScheduler(opId) + } } else { tw.logger.Infof("execute operation %x success: %s", opId, tx.Hash()) @@ -101,6 +107,16 @@ func (tw *WorkerEVM) executeCallSchedule( predecessor := cs[0].(*evmTimelockCallScheduled).callScheduled.Predecessor salt := cs[0].(*evmTimelockCallScheduled).callScheduled.Salt + if tw.maxGasLimit > 0 { + gasEstimate, err := estimateGas(ctx, c, *txOpts, calls, predecessor, salt) + if err != nil { + return nil, fmt.Errorf("failed to estimate gas for execute batch: %w", err) + } + if gasEstimate >= tw.maxGasLimit { + return nil, ErrMaxGasLimit + } + } + return Retry(ctx, func(rctx context.Context) (*types.Transaction, error) { txOpts.Context = rctx return c.ExecuteBatch(txOpts, calls, predecessor, salt) @@ -150,6 +166,23 @@ func (tw *WorkerEVM) signTx(chainID *big.Int) bind.SignerFn { } } +func estimateGas( + ctx context.Context, contract *contracts.RBACTimelockTransactor, txOpts bind.TransactOpts, + calls []contracts.RBACTimelockCall, predecessor, salt [32]byte, +) (uint64, error) { + tx, err := Retry(ctx, func(rctx context.Context) (*types.Transaction, error) { + txOpts.Context = rctx //nolint:fatcontext + txOpts.NoSend = true + + return contract.ExecuteBatch(&txOpts, calls, predecessor, salt) + }) + if err != nil { + return 0, fmt.Errorf("failed to estimate gas for execute batch: %w", err) + } + + return tx.Gas(), nil +} + // privateKeyToAddress is an util function to calculate the addresses of a given private key. // From a private key the public key can be deducted, and with the pubkey is // trivial to calculate the addresses. diff --git a/pkg/timelock/retry.go b/pkg/timelock/retry.go index 5df9e66..4754b70 100644 --- a/pkg/timelock/retry.go +++ b/pkg/timelock/retry.go @@ -9,7 +9,6 @@ import ( var ( retryMinDelay = 500 * time.Millisecond - retryAttempts = uint(5) retryIncrementalDelays = [4]int{500, 2000, 8000, 32000} retryContextTimeout = 30 * time.Second retryOpts = func(ctx context.Context) []retry.Option { diff --git a/pkg/timelock/scheduler.go b/pkg/timelock/scheduler.go index 81e51aa..460a67f 100644 --- a/pkg/timelock/scheduler.go +++ b/pkg/timelock/scheduler.go @@ -58,8 +58,8 @@ type scheduler struct { func newScheduler(tick time.Duration, logger *zap.SugaredLogger, executeFn executeFn) *scheduler { s := &scheduler{ ticker: time.NewTicker(tick), - add: make(chan TimelockCallScheduled), - del: make(chan operationKey), + add: make(chan TimelockCallScheduled, 16), + del: make(chan operationKey, 16), store: make(map[operationKey][]TimelockCallScheduled), busy: false, logger: logger, diff --git a/pkg/timelock/worker_evm.go b/pkg/timelock/worker_evm.go index c2bfcb6..33b1d91 100644 --- a/pkg/timelock/worker_evm.go +++ b/pkg/timelock/worker_evm.go @@ -34,6 +34,7 @@ type WorkerEVM struct { abi *abi.ABI addresses []common.Address fromBlock *big.Int + maxGasLimit uint64 pollPeriod int64 listenerPollPeriod int64 pollSize uint64 @@ -51,7 +52,8 @@ var validNodeUrlSchemesEVM = []string{"http", "https", "ws", "wss"} // It's a singleton, so further executions will retrieve the same timelockWorker. func NewTimelockWorkerEVM( nodeURL, timelockAddress, callProxyAddress, privateKey string, fromBlock *big.Int, - pollPeriod int64, listenerPollPeriod int64, pollSize uint64, dryRun bool, logger *zap.SugaredLogger, + maxGasLimit uint64, pollPeriod int64, listenerPollPeriod int64, pollSize uint64, dryRun bool, + logger *zap.SugaredLogger, ) (*WorkerEVM, error) { // Sanity check on each provided variable before allocating more resources. u, err := url.ParseRequestURI(nodeURL) @@ -130,6 +132,7 @@ func NewTimelockWorkerEVM( abi: timelockABI, addresses: []common.Address{common.HexToAddress(timelockAddress)}, fromBlock: fromBlock, + maxGasLimit: maxGasLimit, pollPeriod: pollPeriod, listenerPollPeriod: listenerPollPeriod, pollSize: pollSize, @@ -400,7 +403,7 @@ func (tw *WorkerEVM) fetchAndDispatchLogs( for _, log := range logs { select { case logCh <- log: - tw.logger.With("log", log).Debug("dispatching log") + tw.logger.With("log", log).Debug("dispatched log") case <-ctx.Done(): tw.logger.Debug("stopped while dispatching logs: incomplete retrieval.") return fromBlock @@ -593,6 +596,7 @@ func (tw *WorkerEVM) startLog() { tw.logger.Infof("\tEOA addresses: %v", wallet) tw.logger.Infof("\tStarting from block: %v", tw.fromBlock) + tw.logger.Infof("\tNetwork's max gas limit: %v", tw.maxGasLimit) tw.logger.Infof("\tPoll Period: %v", time.Duration(tw.pollPeriod*int64(time.Second)).String()) tw.logger.Infof("\tEvent Listener Poll Period: %v", time.Duration(tw.listenerPollPeriod*int64(time.Second)).String()) tw.logger.Infof("\tEvent Listener Poll # Logs: %v", tw.pollSize) diff --git a/pkg/timelock/worker_evm_test.go b/pkg/timelock/worker_evm_test.go index be1b474..34ff5c8 100644 --- a/pkg/timelock/worker_evm_test.go +++ b/pkg/timelock/worker_evm_test.go @@ -13,8 +13,8 @@ import ( func newTestTimelockWorker( t *testing.T, nodeURL, timelockAddress, callProxyAddress, privateKey string, fromBlock *big.Int, - pollPeriod int64, eventListenerPollPeriod int64, eventListenerPollSize uint64, dryRun bool, - logger *zap.SugaredLogger, + maxGasLimit uint64, pollPeriod int64, eventListenerPollPeriod int64, eventListenerPollSize uint64, + dryRun bool, logger *zap.SugaredLogger, ) *WorkerEVM { assert.NotEmpty(t, nodeURL, "nodeURL is empty. Are environment variabes in const_test.go set?") assert.NotEmpty(t, timelockAddress, "nodeURL is empty. Are environment variabes in const_test.go set?") @@ -25,7 +25,7 @@ func newTestTimelockWorker( assert.NotNil(t, logger, "logger is nil. Are environment variabes in const_test.go set?") tw, err := NewTimelockWorkerEVM(nodeURL, timelockAddress, callProxyAddress, privateKey, fromBlock, - pollPeriod, eventListenerPollPeriod, eventListenerPollSize, dryRun, logger) + maxGasLimit, pollPeriod, eventListenerPollPeriod, eventListenerPollSize, dryRun, logger) require.NoError(t, err) require.NotNil(t, tw) @@ -43,6 +43,7 @@ func TestNewTimelockWorkerEVM(t *testing.T) { callProxyAddress string privateKey string fromBlock *big.Int + maxGasLimit uint64 pollPeriod int64 eventListenerPollPeriod int64 eventListenerPollSize uint64 @@ -125,7 +126,7 @@ func TestNewTimelockWorkerEVM(t *testing.T) { tt.setup(&args) got, err := NewTimelockWorkerEVM(args.nodeURL, args.timelockAddress, args.callProxyAddress, - args.privateKey, args.fromBlock, args.pollPeriod, args.eventListenerPollPeriod, + args.privateKey, args.fromBlock, args.maxGasLimit, args.pollPeriod, args.eventListenerPollPeriod, args.eventListenerPollSize, args.dryRun, args.logger) if tt.wantErr == "" { @@ -144,7 +145,7 @@ func TestWorker_startLog(t *testing.T) { rpcURL := runRPCServer(t) testWorker := newTestTimelockWorker(t, rpcURL, testTimelockAddress, testCallProxyAddress, testPrivateKey, - testFromBlock, int64(testPollPeriod), int64(testEventListenerPollPeriod), testEventListenerPollSize, + testFromBlock, testMaxGasLimit, int64(testPollPeriod), int64(testEventListenerPollPeriod), testEventListenerPollSize, testDryRun, testLogger) tests := []struct { diff --git a/tests/containers/geth.go b/tests/containers/geth.go index 390aafb..5408d29 100644 --- a/tests/containers/geth.go +++ b/tests/containers/geth.go @@ -46,12 +46,13 @@ func NewGethContainer(ctx context.Context) (*GethContainer, error) { "--cache.blocklogs", "1024", "--datadir", dataDir, }, - LogConsumerCfg: &testcontainers.LogConsumerConfig{ - Opts: []testcontainers.LogProductionOption{ - testcontainers.WithLogProductionTimeout(10 * time.Second), - }, - Consumers: []testcontainers.LogConsumer{&StdoutLogConsumer{Prefix: "|| "}}, - }, + // uncomment to print containers logs + // LogConsumerCfg: &testcontainers.LogConsumerConfig{ + // Opts: []testcontainers.LogProductionOption{ + // testcontainers.WithLogProductionTimeout(10 * time.Second), + // }, + // Consumers: []testcontainers.LogConsumer{&StdoutLogConsumer{Prefix: "|| "}}, + // }, } gethContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: request, diff --git a/tests/integration/evm/onchain_ops.go b/tests/integration/evm/onchain_ops.go index 67e2d1c..5f08701 100644 --- a/tests/integration/evm/onchain_ops.go +++ b/tests/integration/evm/onchain_ops.go @@ -14,6 +14,9 @@ import ( test_contracts "github.com/smartcontractkit/timelock-worker/tests/contracts" ) +var ExecutorRole = common.HexToHash("0xd8aa0f3194971a2a116679f7c2090f6939c8d4e01a2a8d7e41d55e5351469e63") +var AdminRole = common.HexToHash("0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775") + func DeployTimelock( t *testing.T, ctx context.Context, transactor *bind.TransactOpts, backend Backend, adminAccount common.Address, minDelay *big.Int, @@ -37,6 +40,14 @@ func DeployTimelock( require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) t.Logf("timelock address: %v; deploy transaction: %v", address, transaction.Hash()) + // grant Admin role to itself + transaction, err = contract.GrantRole(transactor, AdminRole, address) + require.NoError(t, err) + backend.Commit() + receipt, err = bind.WaitMined(ctx, backend, transaction) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + return address, transaction, receipt, contract } @@ -56,6 +67,16 @@ func DeployCallProxy( require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) t.Logf("call proxy address: %v; deploy transaction: %v", address, transaction.Hash()) + // grant Executor role to call proxy + timelockContract, err := contracts.NewRBACTimelock(timelockAddress, backend) + require.NoError(t, err) + transaction, err = timelockContract.GrantRole(transactor, ExecutorRole, address) + require.NoError(t, err) + backend.Commit() + receipt, err = bind.WaitMined(ctx, backend, transaction) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + return address, transaction, receipt, contract } diff --git a/tests/integration/evm/timelock_test.go b/tests/integration/evm/timelock_test.go index f345518..140008f 100644 --- a/tests/integration/evm/timelock_test.go +++ b/tests/integration/evm/timelock_test.go @@ -44,7 +44,9 @@ func (s *integrationTestSuite) TestTimelockWorkerListen() { "RoleGranted", // | "RoleGranted", // | "MinDelayChange", // <--+ - "MinDelayChange", // <----- updateDelay call + "RoleGranted", // <----- grantRole(admin, timelock) + "RoleGranted", // <----- grantRole(executor, callproxy) + "MinDelayChange", // <----- updateDelay() call } tests := []struct { @@ -66,7 +68,7 @@ func (s *integrationTestSuite) TestTimelockWorkerListen() { callProxyAddress, _, _, _ := DeployCallProxy(s.T(), ctx, transactor, backend, timelockAddress) go runTimelockWorker(s.T(), sctx, tt.url, timelockAddress.String(), callProxyAddress.String(), - account.HexPrivateKey, big.NewInt(0), int64(60), int64(1), uint64(10), true, logger) + account.HexPrivateKey, big.NewInt(0), uint64(0), int64(60), int64(1), uint64(10), true, logger) UpdateDelay(s.T(), ctx, transactor, backend, timelockContract, big.NewInt(10)) @@ -140,7 +142,7 @@ func (s *integrationTestSuite) TestTimelockWorkerDryRun() { callProxyAddress, _, _, _ := DeployCallProxy(s.T(), tctx, transactor, backend, timelockAddress) go runTimelockWorker(s.T(), tctx, gethURL, timelockAddress.String(), callProxyAddress.String(), - account.HexPrivateKey, big.NewInt(0), int64(1), int64(1), uint64(10), tt.dryRun, logger) + account.HexPrivateKey, big.NewInt(0), uint64(0), int64(1), int64(1), uint64(10), tt.dryRun, logger) ScheduleBatch(s.T(), tctx, transactor, backend, timelockContract, calls, [32]byte{}, [32]byte{}, big.NewInt(1)) @@ -169,7 +171,7 @@ func (s *integrationTestSuite) TestTimelockWorkerCancelledEvent() { callProxyAddress, _, _, _ := DeployCallProxy(s.T(), ctx, transactor, backend, timelockAddress) go runTimelockWorker(s.T(), ctx, gethURL, timelockAddress.String(), callProxyAddress.String(), - account.HexPrivateKey, big.NewInt(0), int64(1), int64(1), uint64(10), false, logger) + account.HexPrivateKey, big.NewInt(0), uint64(0), int64(1), int64(1), uint64(10), false, logger) calls := []contracts.RBACTimelockCall{{ Target: common.HexToAddress("0x000000000000000000000000000000000000000"), @@ -215,7 +217,7 @@ func (s *integrationTestSuite) TestTimelockWorkerPollSize() { // --- act --- go runTimelockWorker(s.T(), ctx, gethURL, timelockAddress.String(), callProxyAddress.String(), - account.HexPrivateKey, big.NewInt(0), int64(1), int64(1), uint64(2), false, logger) + account.HexPrivateKey, big.NewInt(0), uint64(0), int64(1), int64(1), uint64(2), false, logger) // --- assert --- s.Require().EventuallyWithT(func(collect *assert.CollectT) { @@ -225,17 +227,59 @@ func (s *integrationTestSuite) TestTimelockWorkerPollSize() { }, 2*time.Second, 100*time.Millisecond, logMessages(logs)) } +func (s *integrationTestSuite) TestTimelockWorkerMaxGasLimit() { + // --- arrange --- + ctx, cancel := context.WithCancel(s.Ctx) + defer cancel() + + account := NewTestAccount(s.T()) + _, err := s.GethContainer.CreateAccount(ctx, account.HexAddress, account.HexPrivateKey, 1) + s.Require().NoError(err) + + gethURL := s.GethContainer.HTTPConnStr(s.T(), ctx) + backend := NewRPCBackend(s.T(), ctx, gethURL) + transactor := s.KeyedTransactor(account.PrivateKey, nil) + logger, logs := timelockTests.NewTestLogger() + + timelockAddress, _, _, timelockContract := DeployTimelock(s.T(), ctx, transactor, backend, + account.Address, big.NewInt(1)) + callProxyAddress, _, _, _ := DeployCallProxy(s.T(), ctx, transactor, backend, timelockAddress) + + time.Sleep(1 * time.Second) // wait for a few blocks before starting the timelock worker service + + calls := []contracts.RBACTimelockCall{{ + Target: timelockAddress, + Value: big.NewInt(0), + Data: common.FromHex("64d62353000000000000000000000000000000000000000000000000000000000000003c"), // UpdateMinDelay(60) + }} + predecessor := common.Hash{} + salt := common.Hash{} + maxGasLimit := uint64(20000) + + // --- act --- + go runTimelockWorker(s.T(), ctx, gethURL, timelockAddress.String(), callProxyAddress.String(), + account.HexPrivateKey, big.NewInt(0), maxGasLimit, int64(1), int64(1), uint64(2), false, logger) + + ScheduleBatch(s.T(), ctx, transactor, backend, timelockContract, calls, predecessor, salt, big.NewInt(1)) + + // --- assert --- + expectedMessage := "error: transaction gas exceeds max gas limit" + s.Require().EventuallyWithT(func(collect *assert.CollectT) { + assertLogMessageSnippet(collect, logs, expectedMessage) + }, 10*time.Second, 250*time.Millisecond, logMessages(logs)) +} + // ----- helpers ----- func runTimelockWorker( t *testing.T, ctx context.Context, nodeURL, timelockAddress, callProxyAddress, privateKey string, - fromBlock *big.Int, pollPeriod int64, listenerPollPeriod int64, listenerPollSize uint64, - dryRun bool, logger *zap.Logger, + fromBlock *big.Int, maxGasLimit uint64, pollPeriod int64, listenerPollPeriod int64, + listenerPollSize uint64, dryRun bool, logger *zap.Logger, ) { t.Logf("TimelockWorker.Listen(%v, %v, %v, %v, %v, %v, %v, %v)", nodeURL, timelockAddress, callProxyAddress, privateKey, fromBlock, pollPeriod, listenerPollPeriod, listenerPollSize) - timelockWorker, err := timelock.NewTimelockWorkerEVM(nodeURL, timelockAddress, - callProxyAddress, privateKey, fromBlock, pollPeriod, listenerPollPeriod, listenerPollSize, dryRun, logger.Sugar()) + timelockWorker, err := timelock.NewTimelockWorkerEVM(nodeURL, timelockAddress, callProxyAddress, privateKey, + fromBlock, maxGasLimit, pollPeriod, listenerPollPeriod, listenerPollSize, dryRun, logger.Sugar()) require.NoError(t, err) require.NotNil(t, timelockWorker) @@ -247,6 +291,10 @@ func assertLogMessage(t assert.TestingT, logs *observer.ObservedLogs, message st assert.Equal(t, logs.FilterMessage(message).Len(), 1) } +func assertLogMessageSnippet(t assert.TestingT, logs *observer.ObservedLogs, message string) { + assert.Equal(t, logs.FilterMessageSnippet(message).Len(), 1) +} + func logMessages(logs *observer.ObservedLogs) string { m := make([]string, 0, logs.Len()) for _, entry := range logs.All() { From ced769cfc9abaa9053dc34b2bd35c3b23389e01b Mon Sep 17 00:00:00 2001 From: Gustavo Gama Date: Thu, 20 Nov 2025 19:05:18 -0300 Subject: [PATCH 2/2] fix: flaky test --- pkg/timelock/scheduler_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/timelock/scheduler_test.go b/pkg/timelock/scheduler_test.go index 003f7ba..6c8672b 100644 --- a/pkg/timelock/scheduler_test.go +++ b/pkg/timelock/scheduler_test.go @@ -178,13 +178,13 @@ func Test_scheduler_concurrency(t *testing.T) { // run scheduler testScheduler := newScheduler(10*time.Millisecond, logger, execFn) - _ = testScheduler.runScheduler(ctx) + schedulerDone := testScheduler.runScheduler(ctx) // run mock event listener go runMockEventListener(t, ctx, cancel, testScheduler, executedCh, numOps) // wait for all operations to be executed - <-ctx.Done() + <-schedulerDone require.GreaterOrEqual(t, len(executedOps), numOps) executedIDs := lo.Keys(executedOps)