Skip to content

Commit 511aac7

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

11 files changed

Lines changed: 415 additions & 63 deletions

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/ethereum/go-ethereum v1.15.7
1111
github.com/gagliardetto/solana-go v1.12.0
1212
github.com/google/go-cmp v0.7.0
13+
github.com/near/borsh-go v0.3.1
1314
github.com/prometheus/client_golang v1.21.1
1415
github.com/samber/lo v1.47.0
1516
github.com/smartcontractkit/ccip-owner-contracts v0.0.0-20240917103524-56f1a8d2cd4b

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
295295
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
296296
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
297297
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
298+
github.com/near/borsh-go v0.3.1 h1:ukNbhJlPKxfua0/nIuMZhggSU8zvtRP/VyC25LLqPUA=
299+
github.com/near/borsh-go v0.3.1/go.mod h1:NeMochZp7jN/pYFuxLkrZtmLqbADmnp/y1+/dL+AsyQ=
298300
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
299301
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
300302
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=

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: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,42 @@ 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"
1514
"go.uber.org/zap"
1615
)
1716

1817
type operationKey [32]byte
1918

19+
type TimelockCallScheduled interface {
20+
Id() operationKey
21+
Index() int
22+
BlockNumber() *big.Int
23+
TxHash() string
24+
}
25+
2026
type Scheduler interface {
2127
runScheduler(ctx context.Context) <-chan struct{}
22-
addToScheduler(op *contracts.RBACTimelockCallScheduled)
28+
addToScheduler(op TimelockCallScheduled)
2329
delFromScheduler(op operationKey)
2430
dumpOperationStore(now func() time.Time)
2531
}
2632

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

2935
// Scheduler represents a scheduler with an in memory store.
3036
// Whenever accesing the map the mutex should be Locked, to prevent
3137
// any race condition.
3238
type scheduler struct {
3339
mu sync.Mutex
3440
ticker *time.Ticker
35-
add chan *contracts.RBACTimelockCallScheduled
41+
add chan TimelockCallScheduled
3642
del chan operationKey
37-
store map[operationKey][]*contracts.RBACTimelockCallScheduled
43+
store map[operationKey][]TimelockCallScheduled
3844
busy bool
3945
logger *zap.SugaredLogger
4046
executeFn executeFn
@@ -44,9 +50,9 @@ type scheduler struct {
4450
func newScheduler(tick time.Duration, logger *zap.SugaredLogger, executeFn executeFn) *scheduler {
4551
s := &scheduler{
4652
ticker: time.NewTicker(tick),
47-
add: make(chan *contracts.RBACTimelockCallScheduled),
53+
add: make(chan TimelockCallScheduled),
4854
del: make(chan operationKey),
49-
store: make(map[operationKey][]*contracts.RBACTimelockCallScheduled),
55+
store: make(map[operationKey][]TimelockCallScheduled),
5056
busy: false,
5157
logger: logger,
5258
executeFn: executeFn,
@@ -88,12 +94,12 @@ func (tw *scheduler) runScheduler(ctx context.Context) <-chan struct{} {
8894

8995
case op := <-tw.add:
9096
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)
97+
for len(tw.store[op.Id()]) <= op.Index() {
98+
tw.store[op.Id()] = append(tw.store[op.Id()], op)
9399
}
94-
tw.store[op.Id][op.Index.Int64()] = op
100+
tw.store[op.Id()][op.Index()] = op
95101
tw.mu.Unlock()
96-
tw.logger.Debugf("scheduled operation: %x", op.Id)
102+
tw.logger.Debugf("scheduled operation: %x", op.Id())
97103

98104
case op := <-tw.del:
99105
if _, ok := tw.store[op]; ok {
@@ -125,8 +131,8 @@ func (tw *scheduler) updateSchedulerDelay(t time.Duration) {
125131
}
126132

127133
// 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)
134+
func (tw *scheduler) addToScheduler(op TimelockCallScheduled) {
135+
tw.logger.Debugf("scheduling operation: %x", op.Id())
130136
tw.add <- op
131137
}
132138

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

173179
// Get the earliest block from all the operations stored by sorting them.
174-
blocks := make([]uint64, 0)
180+
blocks := make([]*big.Int, 0)
175181
for _, op := range tw.store {
176-
blocks = append(blocks, op[0].Raw.BlockNumber)
182+
blocks = append(blocks, op[0].BlockNumber())
177183
}
178-
slices.Sort(blocks)
184+
slices.SortFunc(blocks, func(a, b *big.Int) int { return a.Cmp(b) })
179185

180186
w := bufio.NewWriter(f)
181187

@@ -185,22 +191,22 @@ func (tw *scheduler) dumpOperationStore(now func() time.Time) {
185191
}
186192

187193
type storeRecord struct {
188-
Block uint64
194+
Block *big.Int
189195
OpKey operationKey
190-
Ops []*contracts.RBACTimelockCallScheduled
196+
Ops []TimelockCallScheduled
191197
}
192198

193199
// writeOperationStore writes the operations to the writer.
194200
func writeOperationStore(
195201
w io.Writer,
196202
logger *zap.SugaredLogger,
197-
store map[operationKey][]*contracts.RBACTimelockCallScheduled,
198-
earliest uint64,
203+
store map[operationKey][]TimelockCallScheduled,
204+
earliest *big.Int,
199205
now func() time.Time,
200206
) {
201207
var (
202208
err error
203-
op *contracts.RBACTimelockCallScheduled
209+
op TimelockCallScheduled
204210
msg string
205211
)
206212

@@ -216,28 +222,24 @@ func writeOperationStore(
216222
continue
217223
}
218224
storeRecords = append(storeRecords, storeRecord{
219-
Block: ops[0].Raw.BlockNumber,
225+
Block: ops[0].BlockNumber(),
220226
OpKey: opID,
221227
Ops: ops,
222228
})
223229
}
224-
sort.Slice(storeRecords, func(i, j int) bool {
225-
return storeRecords[i].Block < storeRecords[j].Block
226-
})
230+
slices.SortFunc(storeRecords, func(a, b storeRecord) int { return a.Block.Cmp(b.Block) })
227231

228232
for _, record := range storeRecords {
229233
op = record.Ops[0]
230234

231-
if op.Raw.BlockNumber == earliest {
235+
if op.BlockNumber().Cmp(earliest) == 0 {
232236
logLine := fmt.Sprintf("earliest unexecuted CallSchedule. Use this block number when "+
233237
"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)
238+
"or using the flag --from-block=%v", op.BlockNumber(), op.BlockNumber())
239+
logger.With(fieldTXHash, op.TxHash()).With(fieldBlockNumber, op.BlockNumber()).Info(logLine)
237240
msg = toEarliestRecord(op)
238241
} else {
239-
logger.With(fieldTXHash, fmt.Sprintf("%x", op.Raw.TxHash[:])).
240-
With(fieldBlockNumber, op.Raw.BlockNumber).Info("CallSchedule pending")
242+
logger.With(fieldTXHash, op.TxHash()).With(fieldBlockNumber, op.BlockNumber()).Info("CallSchedule pending")
241243
msg = toSubsequentRecord(op)
242244
}
243245

@@ -249,17 +251,17 @@ func writeOperationStore(
249251
}
250252

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

257-
return fmt.Sprintf(tmpl, op.Id, op.Raw.BlockNumber, op.Raw.BlockNumber, op.Raw.BlockNumber)
259+
return fmt.Sprintf(tmpl, op.Id(), op.BlockNumber(), op.BlockNumber(), op.BlockNumber())
258260
}
259261

260262
// 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)
263+
func toSubsequentRecord(op TimelockCallScheduled) string {
264+
return fmt.Sprintf("CallSchedule pending ID: %x\tBlock Number: %v\n", op.Id(), op.BlockNumber())
263265
}
264266

265267
// ----- nop scheduler -----
@@ -284,7 +286,7 @@ func (s *nopScheduler) runScheduler(ctx context.Context) <-chan struct{} {
284286
return ch
285287
}
286288

287-
func (s *nopScheduler) addToScheduler(op *contracts.RBACTimelockCallScheduled) {
289+
func (s *nopScheduler) addToScheduler(op TimelockCallScheduled) {
288290
s.logger.With("op", op).Info("nop.addToScheduler")
289291
}
290292

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)