Skip to content

Commit 93c8c2a

Browse files
feat: add interfaces to make scheduler chain-family agnostic
1 parent 621560c commit 93c8c2a

11 files changed

Lines changed: 683 additions & 64 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/avast/retry-go/v4 v4.6.1
99
github.com/docker/go-connections v0.5.0
1010
github.com/ethereum/go-ethereum v1.15.7
11+
github.com/gagliardetto/binary v0.8.0
1112
github.com/gagliardetto/solana-go v1.12.0
1213
github.com/google/go-cmp v0.7.0
1314
github.com/prometheus/client_golang v1.21.1
@@ -63,7 +64,6 @@ require (
6364
github.com/felixge/httpsnoop v1.0.4 // indirect
6465
github.com/fsnotify/fsnotify v1.8.0 // indirect
6566
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
66-
github.com/gagliardetto/binary v0.8.0 // indirect
6767
github.com/gagliardetto/treeout v0.1.4 // indirect
6868
github.com/getsentry/sentry-go v0.27.0 // indirect
6969
github.com/go-logr/logr v1.4.2 // indirect

pkg/timelock/operations_evm.go

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,44 @@ import (
1919
// - The predecessor operation is finished
2020
// - The operation is ready to be executed
2121
// Otherwise the operation will throw an info log and wait for a future tick.
22-
func (tw *WorkerEVM) execute(ctx context.Context, op []*contracts.RBACTimelockCallScheduled) {
23-
isReady, err := isReady(ctx, tw.contract, op[0].Id)
22+
func (tw *WorkerEVM) execute(ctx context.Context, op []TimelockCallScheduled) {
23+
if len(op) == 0 {
24+
tw.logger.Warn("no calls given")
25+
return
26+
}
27+
opId := op[0].Id()
28+
29+
isReady, err := isReady(ctx, tw.contract, opId)
2430
if err != nil {
25-
tw.logger.Errorw("unable to read operation %x \"ready\" status: %s", op[0].Id, err.Error())
31+
tw.logger.Errorw("unable to read operation %x \"ready\" status: %s", opId, err.Error())
2632
return
2733
}
2834
if !isReady {
29-
tw.logger.Infof("skipping operation %x: not ready", op[0].Id)
35+
tw.logger.Infof("skipping operation %x: not ready", opId)
3036
return
3137
}
3238

33-
tw.logger.Debugf("execute operation %x", op[0].Id)
39+
tw.logger.Debugf("execute operation %x", opId)
3440

3541
tx, err := tw.executeCallSchedule(ctx, &tw.executeContract.RBACTimelockTransactor, op, tw.privateKey)
3642
if err != nil || tx == nil {
37-
tw.logger.Errorf("execute operation %x error: %s", op[0].Id, err.Error())
43+
tw.logger.Errorf("execute operation %x error: %s", opId, err.Error())
3844
} else {
39-
tw.logger.Infof("execute operation %x success: %s", op[0].Id, tx.Hash())
45+
tw.logger.Infof("execute operation %x success: %s", opId, tx.Hash())
4046

4147
_, err := Retry(ctx, func(rctx context.Context) (*types.Receipt, error) {
4248
return bind.WaitMined(rctx, tw.ethClient, tx)
4349
})
4450
if err != nil {
45-
tw.logger.Errorf("execute operation %x error: %s", op[0].Id, err.Error())
51+
tw.logger.Errorf("execute operation %x error: %s", opId, err.Error())
4652
}
4753
}
4854
}
4955

5056
// executeCallScheduleOperation is the handler to execute a CallScheduled operation.
51-
func (tw *WorkerEVM) executeCallSchedule(ctx context.Context, c *contracts.RBACTimelockTransactor, cs []*contracts.RBACTimelockCallScheduled, privateKey *ecdsa.PrivateKey) (*types.Transaction, error) {
57+
func (tw *WorkerEVM) executeCallSchedule(
58+
ctx context.Context, c *contracts.RBACTimelockTransactor, cs []TimelockCallScheduled, privateKey *ecdsa.PrivateKey,
59+
) (*types.Transaction, error) {
5260
fromAddress, err := privateKeyToAddress(privateKey)
5361
if err != nil {
5462
return nil, err
@@ -57,10 +65,15 @@ func (tw *WorkerEVM) executeCallSchedule(ctx context.Context, c *contracts.RBACT
5765
// Compute all the different calls from each specific CallSchedule.
5866
calls := make([]contracts.RBACTimelockCall, 0, len(cs))
5967
for _, op := range cs {
68+
evmOp, ok := op.(*evmTimelockCallScheduled)
69+
if !ok {
70+
return nil, fmt.Errorf("invalid operation type: %T (expected *evm.RBACTimelockCallScheduled)", op)
71+
}
72+
6073
calls = append(calls, contracts.RBACTimelockCall{
61-
Target: op.Target,
62-
Value: op.Value,
63-
Data: op.Data,
74+
Target: evmOp.callScheduled.Target,
75+
Value: evmOp.callScheduled.Value,
76+
Data: evmOp.callScheduled.Data,
6477
})
6578
}
6679

@@ -85,9 +98,12 @@ func (tw *WorkerEVM) executeCallSchedule(ctx context.Context, c *contracts.RBACT
8598
tw.logger.Infof("Calling execute Batch...")
8699
// Execute the tx's with all the computed calls.
87100
// Predecessor and salt are the same for all the tx's.
101+
predecessor := cs[0].(*evmTimelockCallScheduled).callScheduled.Predecessor
102+
salt := cs[0].(*evmTimelockCallScheduled).callScheduled.Salt
103+
88104
return Retry(ctx, func(rctx context.Context) (*types.Transaction, error) {
89105
txOpts.Context = rctx
90-
return c.ExecuteBatch(txOpts, calls, cs[0].Predecessor, cs[0].Salt)
106+
return c.ExecuteBatch(txOpts, calls, predecessor, salt)
91107
})
92108
}
93109

pkg/timelock/scheduler.go

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,47 @@ import (
55
"context"
66
"fmt"
77
"io"
8+
"math/big"
89
"os"
910
"slices"
10-
"sort"
1111
"sync"
1212
"time"
1313

14-
contracts "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers"
14+
eth "github.com/ethereum/go-ethereum/common"
1515
"go.uber.org/zap"
1616
)
1717

1818
type operationKey [32]byte
1919

20+
func operationKeyFromHex(hex string) operationKey {
21+
return operationKey(eth.HexToHash(hex))
22+
}
23+
24+
type TimelockCallScheduled interface {
25+
Id() operationKey
26+
Index() int
27+
BlockNumber() *big.Int
28+
TxHash() string
29+
}
30+
2031
type Scheduler interface {
2132
runScheduler(ctx context.Context) <-chan struct{}
22-
addToScheduler(op *contracts.RBACTimelockCallScheduled)
33+
addToScheduler(op TimelockCallScheduled)
2334
delFromScheduler(op operationKey)
2435
dumpOperationStore(now func() time.Time)
2536
}
2637

27-
type executeFn func(context.Context, []*contracts.RBACTimelockCallScheduled)
38+
type executeFn func(context.Context, []TimelockCallScheduled)
2839

2940
// Scheduler represents a scheduler with an in memory store.
3041
// Whenever accesing the map the mutex should be Locked, to prevent
3142
// any race condition.
3243
type scheduler struct {
3344
mu sync.Mutex
3445
ticker *time.Ticker
35-
add chan *contracts.RBACTimelockCallScheduled
46+
add chan TimelockCallScheduled
3647
del chan operationKey
37-
store map[operationKey][]*contracts.RBACTimelockCallScheduled
48+
store map[operationKey][]TimelockCallScheduled
3849
busy bool
3950
logger *zap.SugaredLogger
4051
executeFn executeFn
@@ -44,9 +55,9 @@ type scheduler struct {
4455
func newScheduler(tick time.Duration, logger *zap.SugaredLogger, executeFn executeFn) *scheduler {
4556
s := &scheduler{
4657
ticker: time.NewTicker(tick),
47-
add: make(chan *contracts.RBACTimelockCallScheduled),
58+
add: make(chan TimelockCallScheduled),
4859
del: make(chan operationKey),
49-
store: make(map[operationKey][]*contracts.RBACTimelockCallScheduled),
60+
store: make(map[operationKey][]TimelockCallScheduled),
5061
busy: false,
5162
logger: logger,
5263
executeFn: executeFn,
@@ -88,12 +99,12 @@ func (tw *scheduler) runScheduler(ctx context.Context) <-chan struct{} {
8899

89100
case op := <-tw.add:
90101
tw.mu.Lock()
91-
for len(tw.store[op.Id]) <= int(op.Index.Int64()) {
92-
tw.store[op.Id] = append(tw.store[op.Id], op)
102+
for len(tw.store[op.Id()]) <= op.Index() {
103+
tw.store[op.Id()] = append(tw.store[op.Id()], op)
93104
}
94-
tw.store[op.Id][op.Index.Int64()] = op
105+
tw.store[op.Id()][op.Index()] = op
95106
tw.mu.Unlock()
96-
tw.logger.Debugf("scheduled operation: %x", op.Id)
107+
tw.logger.Debugf("scheduled operation: %x", op.Id())
97108

98109
case op := <-tw.del:
99110
if _, ok := tw.store[op]; ok {
@@ -125,8 +136,8 @@ func (tw *scheduler) updateSchedulerDelay(t time.Duration) {
125136
}
126137

127138
// addToScheduler adds a new CallSchedule operation safely to the store.
128-
func (tw *scheduler) addToScheduler(op *contracts.RBACTimelockCallScheduled) {
129-
tw.logger.Debugf("scheduling operation: %x", op.Id)
139+
func (tw *scheduler) addToScheduler(op TimelockCallScheduled) {
140+
tw.logger.Debugf("scheduling operation: %x", op.Id())
130141
tw.add <- op
131142
}
132143

@@ -171,11 +182,11 @@ func (tw *scheduler) dumpOperationStore(now func() time.Time) {
171182
tw.logger.Infof("generating logs with pending operations in %s", logPath+logFile)
172183

173184
// Get the earliest block from all the operations stored by sorting them.
174-
blocks := make([]uint64, 0)
185+
blocks := make([]*big.Int, 0)
175186
for _, op := range tw.store {
176-
blocks = append(blocks, op[0].Raw.BlockNumber)
187+
blocks = append(blocks, op[0].BlockNumber())
177188
}
178-
slices.Sort(blocks)
189+
slices.SortFunc(blocks, func(a, b *big.Int) int { return a.Cmp(b) })
179190

180191
w := bufio.NewWriter(f)
181192

@@ -185,22 +196,22 @@ func (tw *scheduler) dumpOperationStore(now func() time.Time) {
185196
}
186197

187198
type storeRecord struct {
188-
Block uint64
199+
Block *big.Int
189200
OpKey operationKey
190-
Ops []*contracts.RBACTimelockCallScheduled
201+
Ops []TimelockCallScheduled
191202
}
192203

193204
// writeOperationStore writes the operations to the writer.
194205
func writeOperationStore(
195206
w io.Writer,
196207
logger *zap.SugaredLogger,
197-
store map[operationKey][]*contracts.RBACTimelockCallScheduled,
198-
earliest uint64,
208+
store map[operationKey][]TimelockCallScheduled,
209+
earliest *big.Int,
199210
now func() time.Time,
200211
) {
201212
var (
202213
err error
203-
op *contracts.RBACTimelockCallScheduled
214+
op TimelockCallScheduled
204215
msg string
205216
)
206217

@@ -216,28 +227,24 @@ func writeOperationStore(
216227
continue
217228
}
218229
storeRecords = append(storeRecords, storeRecord{
219-
Block: ops[0].Raw.BlockNumber,
230+
Block: ops[0].BlockNumber(),
220231
OpKey: opID,
221232
Ops: ops,
222233
})
223234
}
224-
sort.Slice(storeRecords, func(i, j int) bool {
225-
return storeRecords[i].Block < storeRecords[j].Block
226-
})
235+
slices.SortFunc(storeRecords, func(a, b storeRecord) int { return a.Block.Cmp(b.Block) })
227236

228237
for _, record := range storeRecords {
229238
op = record.Ops[0]
230239

231-
if op.Raw.BlockNumber == earliest {
240+
if op.BlockNumber().Cmp(earliest) == 0 {
232241
logLine := fmt.Sprintf("earliest unexecuted CallSchedule. Use this block number when "+
233242
"spinning up the service again, with the environment variable or in timelock.env as FROM_BLOCK=%v, "+
234-
"or using the flag --from-block=%v", op.Raw.BlockNumber, op.Raw.BlockNumber)
235-
logger.With(fieldTXHash, fmt.Sprintf("%x", op.Raw.TxHash[:])).
236-
With(fieldBlockNumber, op.Raw.BlockNumber).Info(logLine)
243+
"or using the flag --from-block=%v", op.BlockNumber(), op.BlockNumber())
244+
logger.With(fieldTXHash, op.TxHash()).With(fieldBlockNumber, op.BlockNumber()).Info(logLine)
237245
msg = toEarliestRecord(op)
238246
} else {
239-
logger.With(fieldTXHash, fmt.Sprintf("%x", op.Raw.TxHash[:])).
240-
With(fieldBlockNumber, op.Raw.BlockNumber).Info("CallSchedule pending")
247+
logger.With(fieldTXHash, op.TxHash()).With(fieldBlockNumber, op.BlockNumber()).Info("CallSchedule pending")
241248
msg = toSubsequentRecord(op)
242249
}
243250

@@ -249,17 +256,17 @@ func writeOperationStore(
249256
}
250257

251258
// toEarliestRecord returns a string with the earliest record.
252-
func toEarliestRecord(op *contracts.RBACTimelockCallScheduled) string {
259+
func toEarliestRecord(op TimelockCallScheduled) string {
253260
tmpl := "Earliest CallSchedule pending ID: %x\tBlock Number: %v\n" +
254261
"\tUse this block number to ensure all pending operations are properly executed. " +
255262
"\tSet it as environment variable or in timelock.env with FROM_BLOCK=%v, or as a flag with --from-block=%v\n"
256263

257-
return fmt.Sprintf(tmpl, op.Id, op.Raw.BlockNumber, op.Raw.BlockNumber, op.Raw.BlockNumber)
264+
return fmt.Sprintf(tmpl, op.Id(), op.BlockNumber(), op.BlockNumber(), op.BlockNumber())
258265
}
259266

260267
// toSubsequentRecord returns a string for use with each subsequent record sent to a writer.
261-
func toSubsequentRecord(op *contracts.RBACTimelockCallScheduled) string {
262-
return fmt.Sprintf("CallSchedule pending ID: %x\tBlock Number: %v\n", op.Id, op.Raw.BlockNumber)
268+
func toSubsequentRecord(op TimelockCallScheduled) string {
269+
return fmt.Sprintf("CallSchedule pending ID: %x\tBlock Number: %v\n", op.Id(), op.BlockNumber())
263270
}
264271

265272
// ----- nop scheduler -----
@@ -284,7 +291,7 @@ func (s *nopScheduler) runScheduler(ctx context.Context) <-chan struct{} {
284291
return ch
285292
}
286293

287-
func (s *nopScheduler) addToScheduler(op *contracts.RBACTimelockCallScheduled) {
294+
func (s *nopScheduler) addToScheduler(op TimelockCallScheduled) {
288295
s.logger.With("op", op).Info("nop.addToScheduler")
289296
}
290297

pkg/timelock/scheduler_evm.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package timelock
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
bindings "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers"
8+
)
9+
10+
var _ TimelockCallScheduled = NewEVMTimelockCallScheduled(nil)
11+
12+
type evmTimelockCallScheduled struct {
13+
callScheduled *bindings.RBACTimelockCallScheduled
14+
}
15+
16+
func NewEVMTimelockCallScheduled(callScheduled *bindings.RBACTimelockCallScheduled) TimelockCallScheduled {
17+
return &evmTimelockCallScheduled{
18+
callScheduled: callScheduled,
19+
}
20+
}
21+
22+
func (cs *evmTimelockCallScheduled) Id() operationKey {
23+
return cs.callScheduled.Id
24+
}
25+
26+
func (cs *evmTimelockCallScheduled) Index() int {
27+
return int(cs.callScheduled.Index.Int64())
28+
}
29+
30+
func (cs *evmTimelockCallScheduled) BlockNumber() *big.Int {
31+
return new(big.Int).SetUint64(cs.callScheduled.Raw.BlockNumber)
32+
}
33+
34+
func (cs *evmTimelockCallScheduled) TxHash() string {
35+
return fmt.Sprintf("%x", cs.callScheduled.Raw.TxHash[:])
36+
}

pkg/timelock/scheduler_evm_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package timelock
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/core/types"
8+
bindings "github.com/smartcontractkit/ccip-owner-contracts/gethwrappers"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestNewEVMTimelockCallScheduled(t *testing.T) {
13+
callScheduled := makeTestCallScheduled()
14+
evmCallScheduled := NewEVMTimelockCallScheduled(callScheduled)
15+
assert.NotNil(t, evmCallScheduled)
16+
}
17+
18+
func TestEVMTimelockCallScheduled_Id(t *testing.T) {
19+
callScheduled := makeTestCallScheduled()
20+
evmCallScheduled := NewEVMTimelockCallScheduled(callScheduled)
21+
assert.Equal(t, operationKey(callScheduled.Id), evmCallScheduled.Id())
22+
}
23+
24+
func TestEVMTimelockCallScheduled_Index(t *testing.T) {
25+
callScheduled := makeTestCallScheduled()
26+
evmCallScheduled := NewEVMTimelockCallScheduled(callScheduled)
27+
assert.Equal(t, 42, evmCallScheduled.Index())
28+
}
29+
30+
func TestEVMTimelockCallScheduled_BlockNumber(t *testing.T) {
31+
callScheduled := makeTestCallScheduled()
32+
evmCallScheduled := NewEVMTimelockCallScheduled(callScheduled)
33+
assert.Equal(t, big.NewInt(123456), evmCallScheduled.BlockNumber())
34+
}
35+
36+
func TestEVMTimelockCallScheduled_TxHash(t *testing.T) {
37+
callScheduled := makeTestCallScheduled()
38+
evmCallScheduled := NewEVMTimelockCallScheduled(callScheduled)
39+
expected := "aabbcc0000000000000000000000000000000000000000000000000000000000"
40+
assert.Equal(t, expected, evmCallScheduled.TxHash())
41+
}
42+
43+
// ----- helpers ------
44+
45+
func makeTestCallScheduled() *bindings.RBACTimelockCallScheduled {
46+
return &bindings.RBACTimelockCallScheduled{
47+
Id: [32]byte{1, 2, 3},
48+
Index: big.NewInt(42),
49+
Raw: types.Log{
50+
BlockNumber: 123456,
51+
TxHash: [32]byte{0xaa, 0xbb, 0xcc},
52+
},
53+
}
54+
}

0 commit comments

Comments
 (0)