Skip to content

Commit 7825c53

Browse files
Coordination windows performance improvements (#3864)
## Summary - `FindDepositsToSweep` now limits `DepositRevealed` event scanning to the most recent ~30 days (`DepositSweepLookBackBlocks = 216000` blocks) instead of querying from block 0, significantly reducing RPC load on long-running nodes - Includes underflow protection: when `currentBlock < 216000`, `filterStartBlock` remains `0` to avoid uint64 wraparound - `FindDeposits` (used by the MovingFunds safety guard) continues to scan from block 0 to preserve full-history coverage for wallet safety checks - Unswept deposits are now grouped by vault address (case-insensitive) before sweep proposal; the largest group is selected to maximize per-transaction throughput, since deposits targeting different vaults cannot be swept together - Vault=0x0 (nil-vault) deposits excluded from the selected group are logged at Warn level for operator follow-up; they remain recoverable via later sweep cycles, depositor refunds, or reinitializer re-assignment - The `Vault` field is propagated through `DepositReference` and `Deposit` structs for downstream consumers - DepositSweep and MovedFundsSweep coordination actions switch from every 4th coordination window to every window at block 24559289 (~March 1st 2026); MovingFunds retains the 4th-window guard to avoid multiplying its full-history chain scan load ## Coordination actions — before and after **Before:** | Priority | Action | Frequency | Lookback | |----------|--------|-----------|----------| | 0 | ActionRedemption | Every window | Full history (block 0) — `redemptionTimeout` was ~20 years, exceeds chain height | | 1 | ActionDepositSweep | Every 4th window | Full history (block 0) | | 2 | ActionMovedFundsSweep | Every 4th window | Full history (block 0) | | 3 | ActionMovingFunds | Every 4th window | Full history (block 0) | | 4 | ActionHeartbeat | 6.25% probability | No chain scan | **After (block >= 24559289):** | Priority | Action | Frequency | Lookback | |----------|--------|-----------|----------| | 0 | ActionRedemption | Every window | 94,600 blocks (~13.14 days) — `redemptionTimeout` changed to 13 days on-chain | | 1 | ActionDepositSweep | Every window | 216,000 blocks (~30 days) | | 2 | ActionMovedFundsSweep | Every window | 216,000 blocks (~30 days) | | 3 | ActionMovingFunds | Every 4th window | Full history (block 0) | | 4 | ActionHeartbeat | 6.25% probability | No chain scan | ## Changes **Coordination layer** (`coordination.go`): - Add exported `DepositSweepEveryWindowActivationBlock` constant set to block 24559289 (~March 1st 2026 00:00 UTC) - Add `coordinationBlock` parameter to `getActionsChecklist` - Before activation block: all three actions (DepositSweep, MovedFundsSweep, MovingFunds) gated to every 4th window - After activation block: DepositSweep and MovedFundsSweep run on every coordination window; MovingFunds stays gated to every 4th window to protect against excessive full-history scans **Coordination tests** (`coordination_test.go`): - `TestCoordinationExecutor_GetActionsChecklist` covers pre-activation behavior (blocks below 24559289): non-4th windows get only Redemption, 4th windows get all actions - `TestCoordinationExecutor_GetActionsChecklist_PostActivation` covers post-activation behavior (blocks above 24559289) with safety invariant assertions - `assertPostActivationSafety` helper — verifies ActionRedemption at index 0, DepositSweep and MovedFundsSweep always present, MovingFunds absent on non-4th windows - `assertChecklistOrdering` helper — verifies canonical priority ordering across the checklist **Production code** (`deposit_sweep.go`): - Add exported `DepositSweepLookBackBlocks` constant (216000 blocks, ~30 days at 12s/block) - Add `filterStartBlock` parameter to internal `findDeposits()` function - `FindDepositsToSweep()` computes bounded start block via `BlockCounter.CurrentBlock()` with underflow guard - `FindDeposits()` passes `filterStartBlock=0` to preserve full-history behavior - Set `DepositRevealedEventFilter.StartBlock` from the computed value - Group unswept deposits by vault address with case-insensitive normalization - Select the largest vault group for sweep proposal (largest-group-first policy) - Log multi-group scenarios at Info level; log excluded nil-vault deposits at Warn level - Propagate `Vault` field through `Deposit` and `DepositReference` structs **Test helpers** (`tbtcpgtest.go`): - Add `Vault` field to test `Deposit` struct and `DepositsReferences()` output **Tests** (`deposit_sweep_test.go`): - Add `TestDepositSweepLookBackBlocks` — validates the constant equals 216000 - Add `TestDepositSweepTask_FindDepositsToSweep_BoundedLookback` — exercises the bounded path with `currentBlock=300000` - Add `TestDepositSweepTask_FindDepositsToSweep_UnderflowGuard` — exercises the underflow path with `currentBlock=100000` - Wire `MockBlockCounter` into existing JSON-driven scenario tests - Add `TestFindDepositsToSweep_VaultGrouping` with 10 sub-tests covering nil vaults, mixed vaults, case normalization, tied groups, and multi-group selection ## Test plan - [ ] `go test ./pkg/tbtcpg/... -v` — all tests pass - [ ] `go test ./pkg/tbtc/... -run TestCoordinationExecutor_GetActionsChecklist -v` — all coordination tests pass - [ ] `go build ./pkg/...` — compiles cleanly - [ ] `go vet ./pkg/...` — no new issues - [ ] Verify `FindDeposits` (MovingFunds path) still uses `filterStartBlock=0` - [ ] Vault grouping selects correct largest group in multi-vault scenarios - [ ] Nil-vault deposits are logged but not lost (recoverable paths documented) - [ ] Pre-activation: all three actions gated to every 4th window - [ ] Post-activation (block >= 24559289): DepositSweep and MovedFundsSweep on every window - [ ] Post-activation: MovingFunds remains gated to every 4th window - [ ] All operators must upgrade before block 24559289 (~March 1st 2026)
2 parents 7fd2319 + 50181dd commit 7825c53

File tree

5 files changed

+1369
-20
lines changed

5 files changed

+1369
-20
lines changed

pkg/tbtc/coordination.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ const (
5959
// and before they are filtered out as not interesting for the follower,
6060
// they are buffered in the channel.
6161
coordinationMessageReceiveBuffer = 512
62+
63+
// DepositSweepEveryWindowActivationBlock is the Ethereum block height at
64+
// which DepositSweep and MovedFundsSweep actions become available on every
65+
// coordination window instead of every 4th window only. All operators must
66+
// upgrade to a binary containing this constant before the activation block
67+
// is reached.
68+
DepositSweepEveryWindowActivationBlock = uint64(24559289)
6269
)
6370

6471
// errCoordinationExecutorBusy is an error returned when the coordination
@@ -387,7 +394,7 @@ func (ce *coordinationExecutor) coordinate(
387394

388395
execLogger.Infof("coordination leader is: [%s]", leader)
389396

390-
actionsChecklist := ce.getActionsChecklist(window.index(), seed)
397+
actionsChecklist := ce.getActionsChecklist(window.index(), seed, window.coordinationBlock)
391398

392399
execLogger.Infof("actions checklist is: [%v]", actionsChecklist)
393400

@@ -575,6 +582,7 @@ func (ce *coordinationExecutor) getLeader(seed [32]byte) chain.Address {
575582
func (ce *coordinationExecutor) getActionsChecklist(
576583
windowIndex uint64,
577584
seed [32]byte,
585+
coordinationBlock uint64,
578586
) []WalletActionType {
579587
// Return nil checklist for incorrect coordination windows.
580588
if windowIndex == 0 {
@@ -591,16 +599,36 @@ func (ce *coordinationExecutor) getActionsChecklist(
591599
// frequency is every 4 coordination windows.
592600
frequencyWindows := uint64(4)
593601

594-
if windowIndex%frequencyWindows == 0 {
595-
actions = append(actions, ActionDepositSweep)
596-
}
602+
// The activation gate determines how often DepositSweep and
603+
// MovedFundsSweep actions are checked. Before the activation block,
604+
// all three actions (DepositSweep, MovedFundsSweep, MovingFunds)
605+
// are gated by the frequency window. After the activation block,
606+
// DepositSweep and MovedFundsSweep are checked on every coordination
607+
// window while MovingFunds stays frequency-gated because its
608+
// proposal generator performs a full-history chain scan.
609+
if coordinationBlock < DepositSweepEveryWindowActivationBlock {
610+
if windowIndex%frequencyWindows == 0 {
611+
actions = append(actions, ActionDepositSweep)
612+
}
613+
614+
if windowIndex%frequencyWindows == 0 {
615+
actions = append(actions, ActionMovedFundsSweep)
616+
}
597617

598-
if windowIndex%frequencyWindows == 0 {
618+
if windowIndex%frequencyWindows == 0 {
619+
actions = append(actions, ActionMovingFunds)
620+
}
621+
} else {
622+
actions = append(actions, ActionDepositSweep)
599623
actions = append(actions, ActionMovedFundsSweep)
600-
}
601624

602-
if windowIndex%frequencyWindows == 0 {
603-
actions = append(actions, ActionMovingFunds)
625+
// MovingFunds retains the frequency guard because its proposal
626+
// generator (MovingFundsTask.Run) calls FindDeposits which scans
627+
// from block 0, i.e. the full Ethereum history. Removing this
628+
// guard would multiply the scan load proportionally.
629+
if windowIndex%frequencyWindows == 0 {
630+
actions = append(actions, ActionMovingFunds)
631+
}
604632
}
605633

606634
// #nosec G404 (insecure random number source (rand))

0 commit comments

Comments
 (0)