Skip to content

Commit 4e53d4d

Browse files
feat: add IsOperationPending() and IsChainPending() to TimelockExecutable (#368)
This will help us ensure a failed Timelock "schedule" call that failed in the CLD workflow can safely be re-executed. [DX-611](https://smartcontract-it.atlassian.net/browse/DX-611) [DX-611]: https://smartcontract-it.atlassian.net/browse/DX-611?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent e02a96b commit 4e53d4d

File tree

4 files changed

+142
-46
lines changed

4 files changed

+142
-46
lines changed

.changeset/mean-students-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": minor
3+
---
4+
5+
feat: add IsOperationPending() and IsChainPending() to TimelockExecutable

errors.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@ type OperationNotReadyError struct {
1515
}
1616

1717
// Error implements the error interface.
18-
func (e *OperationNotReadyError) Error() string {
18+
func (e OperationNotReadyError) Error() string {
1919
return fmt.Sprintf("operation %d is not ready", e.OpIndex)
2020
}
2121

22+
// OperationNotPendingError is returned when an operation is not yet pending.
23+
type OperationNotPendingError struct {
24+
OpIndex int
25+
}
26+
27+
// Error implements the error interface.
28+
func (e OperationNotPendingError) Error() string {
29+
return fmt.Sprintf("operation %d is not pending", e.OpIndex)
30+
}
31+
2232
// OperationNotDoneError is returned when an operation is not yet done.
2333
type OperationNotDoneError struct {
2434
OpIndex int
2535
}
2636

2737
// Error implements the error interface.
28-
func (e *OperationNotDoneError) Error() string {
38+
func (e OperationNotDoneError) Error() string {
2939
return fmt.Sprintf("operation %d is not done", e.OpIndex)
3040
}
3141

timelock_executable.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,43 @@ func (t *TimelockExecutable) IsOperationReady(ctx context.Context, idx int) erro
131131
return nil
132132
}
133133

134+
// IsChainPending checks if the chain is pending for execution.
135+
func (t *TimelockExecutable) IsChainPending(ctx context.Context, chainSelector types.ChainSelector) error {
136+
// Check readiness for each global operation in the proposal
137+
for globalIndex, op := range t.proposal.Operations {
138+
if op.ChainSelector == chainSelector {
139+
err := t.IsOperationPending(ctx, globalIndex)
140+
if err != nil {
141+
return err
142+
}
143+
}
144+
}
145+
146+
return nil
147+
}
148+
149+
func (t *TimelockExecutable) IsOperationPending(ctx context.Context, idx int) error {
150+
op := t.proposal.Operations[idx]
151+
152+
cs := op.ChainSelector
153+
timelock := t.proposal.TimelockAddresses[cs]
154+
155+
operationID, err := t.GetOpID(ctx, idx, op, cs)
156+
if err != nil {
157+
return fmt.Errorf("unable to get operation ID: %w", err)
158+
}
159+
160+
isPending, err := t.executors[cs].IsOperationPending(ctx, timelock, operationID)
161+
if err != nil {
162+
return err
163+
}
164+
if !isPending {
165+
return &OperationNotPendingError{OpIndex: idx}
166+
}
167+
168+
return nil
169+
}
170+
134171
// IsChainDone checks if the chain is done executing
135172
func (t *TimelockExecutable) IsChainDone(ctx context.Context, chainSelector types.ChainSelector) error {
136173
// Check readiness for each global operation in the proposal

timelock_executable_test.go

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,10 @@ func Test_ScheduleAndCancelProposal(t *testing.T) {
359359
}
360360
}
361361

362-
func scheduleGrantRolesProposal(t *testing.T, ctx context.Context, targetRoles []common.Hash, delay types.Duration) (evmsim.SimulatedChain, *bindings.ManyChainMultiSig, *bindings.RBACTimelock, TimelockProposal, []common.Hash) {
362+
func scheduleGrantRolesProposal(
363+
t *testing.T, targetRoles []common.Hash, delay types.Duration) (evmsim.SimulatedChain, *bindings.ManyChainMultiSig,
364+
*bindings.RBACTimelock, TimelockProposal, []common.Hash,
365+
) {
363366
t.Helper()
364367

365368
sim := evmsim.NewSimulatedChain(t, 1)
@@ -450,7 +453,7 @@ func scheduleGrantRolesProposal(t *testing.T, ctx context.Context, targetRoles [
450453
func scheduleAndExecuteGrantRolesProposal(t *testing.T, ctx context.Context, targetRoles []common.Hash) {
451454
t.Helper()
452455

453-
sim, mcmC, timelockC, proposal, _ := scheduleGrantRolesProposal(t, ctx, targetRoles, types.MustParseDuration("5s"))
456+
sim, mcmC, timelockC, proposal, _ := scheduleGrantRolesProposal(t, targetRoles, types.MustParseDuration("5s"))
454457

455458
converters := map[types.ChainSelector]sdk.TimelockConverter{
456459
chaintest.Chain1Selector: &evm.TimelockConverter{},
@@ -559,58 +562,27 @@ func scheduleAndExecuteGrantRolesProposal(t *testing.T, ctx context.Context, tar
559562
require.False(t, isOperationReady)
560563
}
561564

562-
// Check IsReady function fails
563-
err = tExecutable.IsReady(ctx)
564-
require.Error(t, err)
565-
566-
// Check IsChainReady function fails
567-
for chainSelector := range proposal.ChainMetadata {
568-
err = tExecutable.IsChainReady(ctx, chainSelector)
569-
require.Error(t, err)
570-
}
565+
opIdx := 0
566+
requireOperationPending(t, ctx, tExecutable, &proposal, opIdx)
567+
requireOperationNotReady(t, ctx, tExecutable, &proposal, opIdx)
568+
requireOperationNotDone(t, ctx, tExecutable, &proposal, opIdx)
571569

572570
// sleep for 5 seconds and then mine a block
573571
require.NoError(t, sim.Backend.AdjustTime(5*time.Second))
574572
sim.Backend.Commit() // Note < 1.14 geth needs a commit after adjusting time.
575573

576-
opIdx := 0
577-
578-
// IsReady
579-
err = tExecutable.IsReady(ctx)
580-
require.NoError(t, err)
581-
for chainSelector := range proposal.ChainMetadata {
582-
err = tExecutable.IsChainReady(ctx, chainSelector)
583-
require.NoError(t, err)
584-
}
585-
586-
// !IsDone
587-
err = tExecutable.IsOperationDone(ctx, opIdx)
588-
require.Error(t, err)
589-
for chainSelector := range proposal.ChainMetadata {
590-
err = tExecutable.IsChainDone(ctx, chainSelector)
591-
require.Error(t, err)
592-
}
574+
requireOperationPending(t, ctx, tExecutable, &proposal, opIdx)
575+
requireOperationReady(t, ctx, tExecutable, &proposal, opIdx)
576+
requireOperationNotDone(t, ctx, tExecutable, &proposal, opIdx)
593577

594578
// Execute the proposal
595579
_, err = tExecutable.Execute(ctx, opIdx)
596580
require.NoError(t, err)
597581
sim.Backend.Commit()
598582

599-
// IsDone
600-
err = tExecutable.IsOperationDone(ctx, opIdx)
601-
require.NoError(t, err)
602-
for chainSelector := range proposal.ChainMetadata {
603-
err = tExecutable.IsChainDone(ctx, chainSelector)
604-
require.NoError(t, err)
605-
}
606-
607-
// !IsReady
608-
err = tExecutable.IsOperationReady(ctx, opIdx)
609-
require.Error(t, err)
610-
for chainSelector := range proposal.ChainMetadata {
611-
err = tExecutable.IsChainReady(ctx, chainSelector)
612-
require.Error(t, err)
613-
}
583+
requireOperationNotPending(t, ctx, tExecutable, &proposal, opIdx)
584+
requireOperationNotReady(t, ctx, tExecutable, &proposal, opIdx)
585+
requireOperationDone(t, ctx, tExecutable, &proposal, opIdx)
614586

615587
// Check the state of the timelock contract
616588
for _, role := range targetRoles {
@@ -626,7 +598,7 @@ func scheduleAndExecuteGrantRolesProposal(t *testing.T, ctx context.Context, tar
626598
func scheduleAndCancelGrantRolesProposal(t *testing.T, ctx context.Context, targetRoles []common.Hash) {
627599
t.Helper()
628600

629-
sim, mcmC, timelockC, proposal, _ := scheduleGrantRolesProposal(t, ctx, targetRoles, types.MustParseDuration("5m"))
601+
sim, mcmC, timelockC, proposal, _ := scheduleGrantRolesProposal(t, targetRoles, types.MustParseDuration("5m"))
630602

631603
converters := map[types.ChainSelector]sdk.TimelockConverter{
632604
chaintest.Chain1Selector: &evm.TimelockConverter{},
@@ -985,3 +957,75 @@ func Test_TimelockExecutable_GetChainSpecificIndex(t *testing.T) {
985957
}
986958
})
987959
}
960+
961+
func requireOperationPending(
962+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
963+
) {
964+
t.Helper()
965+
err := tExecutable.IsOperationPending(ctx, opIdx)
966+
require.NoError(t, err)
967+
for chainSelector := range proposal.ChainMetadata {
968+
err = tExecutable.IsChainPending(ctx, chainSelector)
969+
require.NoError(t, err)
970+
}
971+
}
972+
973+
func requireOperationNotPending(
974+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
975+
) {
976+
t.Helper()
977+
err := tExecutable.IsOperationPending(ctx, opIdx)
978+
require.ErrorContains(t, err, "operation 0 is not pending")
979+
for chainSelector := range proposal.ChainMetadata {
980+
err = tExecutable.IsChainPending(ctx, chainSelector)
981+
require.ErrorContains(t, err, "operation 0 is not pending")
982+
}
983+
}
984+
985+
func requireOperationReady(
986+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
987+
) {
988+
t.Helper()
989+
err := tExecutable.IsOperationReady(ctx, opIdx)
990+
require.NoError(t, err)
991+
for chainSelector := range proposal.ChainMetadata {
992+
err = tExecutable.IsChainReady(ctx, chainSelector)
993+
require.NoError(t, err)
994+
}
995+
}
996+
997+
func requireOperationNotReady(
998+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
999+
) {
1000+
t.Helper()
1001+
err := tExecutable.IsOperationReady(ctx, opIdx)
1002+
require.ErrorContains(t, err, "operation 0 is not ready")
1003+
for chainSelector := range proposal.ChainMetadata {
1004+
err = tExecutable.IsChainReady(ctx, chainSelector)
1005+
require.ErrorContains(t, err, "operation 0 is not ready")
1006+
}
1007+
}
1008+
1009+
func requireOperationDone(
1010+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
1011+
) {
1012+
t.Helper()
1013+
err := tExecutable.IsOperationDone(ctx, opIdx)
1014+
require.NoError(t, err)
1015+
for chainSelector := range proposal.ChainMetadata {
1016+
err = tExecutable.IsChainDone(ctx, chainSelector)
1017+
require.NoError(t, err)
1018+
}
1019+
}
1020+
1021+
func requireOperationNotDone(
1022+
t *testing.T, ctx context.Context, tExecutable *TimelockExecutable, proposal *TimelockProposal, opIdx int,
1023+
) {
1024+
t.Helper()
1025+
err := tExecutable.IsOperationDone(ctx, opIdx)
1026+
require.ErrorContains(t, err, "operation 0 is not done")
1027+
for chainSelector := range proposal.ChainMetadata {
1028+
err = tExecutable.IsChainDone(ctx, chainSelector)
1029+
require.ErrorContains(t, err, "operation 0 is not done")
1030+
}
1031+
}

0 commit comments

Comments
 (0)