Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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")
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/timelock/const_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
testCallProxyAddress = "0x0000000000000000000000000000000000000000"
testPrivateKey = "8064bf62c044d2654705b9d0cfbd666c2649fabb76ed8f4b9d8d3eb28267e3cf"
testFromBlock = big.NewInt(0)
testMaxGasLimit = uint64(0)
testPollPeriod = 5
testEventListenerPollPeriod = 1
testEventListenerPollSize = uint64(10)
Expand Down
33 changes: 33 additions & 0 deletions pkg/timelock/operations_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package timelock
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"

Expand All @@ -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
Expand All @@ -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())

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
1 change: 0 additions & 1 deletion pkg/timelock/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/timelock/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions pkg/timelock/scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions pkg/timelock/worker_evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type WorkerEVM struct {
abi *abi.ABI
addresses []common.Address
fromBlock *big.Int
maxGasLimit uint64
pollPeriod int64
listenerPollPeriod int64
pollSize uint64
Expand All @@ -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)
Expand Down Expand Up @@ -130,6 +132,7 @@ func NewTimelockWorkerEVM(
abi: timelockABI,
addresses: []common.Address{common.HexToAddress(timelockAddress)},
fromBlock: fromBlock,
maxGasLimit: maxGasLimit,
pollPeriod: pollPeriod,
listenerPollPeriod: listenerPollPeriod,
pollSize: pollSize,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 6 additions & 5 deletions pkg/timelock/worker_evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?")
Expand All @@ -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)

Expand All @@ -43,6 +43,7 @@ func TestNewTimelockWorkerEVM(t *testing.T) {
callProxyAddress string
privateKey string
fromBlock *big.Int
maxGasLimit uint64
pollPeriod int64
eventListenerPollPeriod int64
eventListenerPollSize uint64
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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 {
Expand Down
13 changes: 7 additions & 6 deletions tests/containers/geth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/evm/onchain_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand All @@ -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
}

Expand Down
Loading
Loading