feat: rolling-window confirmations - #71
Open
karim-en wants to merge 9 commits into
Open
Conversation
olga24912
marked this pull request as ready for review
August 26, 2026 09:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rolling-window confirmations — trade-off notes
Notes on two behavioural consequences of moving the confirmations check from a
per-block cumulative amount to a rolling window of blocks.
What changed
A deposit's required confirmations used to be derived from the total bridged
from its own block. Every new BTC block started that total from zero, so the
same tier cap could be spent again in block N, N+1, N+2, … and a d-deep reorg
could invalidate
d × capinstead ofcap.The check is now: bridging an amount from a block at depth
dmust keep everywindow
[tip − k + 1, tip]withk ≥ dwithin the tier thatkconfirmations buy. Reading the tier table as a rate limit:
Two consequences are worth knowing about before deployment.
1. Raising a tier temporarily under-counts older blocks
What happens
Per-block amounts live in a fixed-capacity ring, one slot per block height,
reused round-robin. Capacity is sized from the config:
Capacity always exceeds the widest window, so under a fixed config no height a
window reads can have been evicted. An evicted slot reads as "nothing was
bridged here" rather than "unknown".
Raising a tier breaks that pairing for one window's worth of time. The window
widens immediately, but the ring only ever recorded the heights that fit the
old, smaller capacity. Heights between the old capacity and the new window
width read as zero, so the window total comes out below the true exposure and
deposits clear at a lower tier than the table intends.
Shrinking is safe: two heights that collide at the smaller capacity are at least
that capacity apart, so they can never both sit inside one window.
Example
Tiers
2 conf → 3 BTC,4 conf → 10 BTC,10 conf → 15 BTC; capacity 17,window 10. Tip is at block 1029, and the DAO raises the top tier to 30
confirmations (window 30, capacity 37).
A new 4 BTC deposit verified at 25 confirmations:
line → the table wanted 30 confirmations → should have been rejected
Pinned by
test_ring_grow_undercounts_an_evicted_block.Impact
max_windowfresh blocks have passed, every height in everywindow was recorded under the new capacity. ~5 hours at a 30-block BTC window.
gap appears precisely when governance is trying to be more conservative.
confirmations); this one fails open.
Options
set
reliable_from = highest_seen + 1 − old_capacityand require max-tierconfirmations for any window reaching below it. Costs one
u64of state, andfor
max_windowblocks after a tier raise, deposits shallower than the maxtier are rejected.
Status: open — pending a decision. Option 2 is the only one that closes the
fail-open direction.
Operational note
Prefer raising tiers during quiet periods, and treat the
max_windowblocksfollowing a raise as a window in which the new, stricter cap is not yet fully
enforced.
2. Window budgets are shared between depositors
What happens
A tier cap is no longer per-deposit or per-block — it is the bridge's total
allowance for a span of blocks, first come first served. A deposit that would
have cleared at the low tier can be pushed to a higher one by anyone else
bridging into any of the
max_window − 1neighbouring blocks. Previously anattacker had to land a transaction in the victim's own block, which is not
something you can choose, and by then that block is already mined.
Note that amounts count from the moment they are verified, not mined, so
ordering is decided by relayer submission order, not by block order.
Example
Tiers as above (
2 conf → 3 BTC). Alice's 2 BTC lands in block 1000.Alice waited two extra blocks because someone else spent the shared allowance in
a different block.
Impact
max_windowconfirmationsinstead of the deposit's own tier.
max_tier_confirmations + deltadeep, because the widest window is satisfiedby every amount the table can rate.
and pay the miner fee, and their deposit consumes the same allowance they are
denying.
effect. This is the rate limit working as designed, not a defect.
Guidance for relayers and integrators
get_required_confirmations(block_height, amount, relayer_account_id, has_extra_msg); it returns the minimum depth that currently clears everywindow.
someone bridges into a neighbouring block after the quote, and it falls again
as the tip advances. Treat a
Not enough confirmations for the rolling-window bridge amountfailure as retryable, not terminal.that would otherwise sit in the lowest tier.
confirmations_delta, which both lowers therequirement and narrows the window they are measured against.
Sustained throughput
Because a gap in the confirmation ladder stretches the previous cap across the
gap, a tier's cap binds up to one confirmation below the next tier's value. For
2 conf → 3 BTC,4 conf → 10 BTC,10 conf → 15 BTC:≈144 BTC/day at 2 confirmations, ≈160 BTC/day at 4. Closing ladder gaps (2 → 3 →
4 …) narrows each cap to its own window, at the cost of one loop iteration and
one ring slot per additional distinct tier value.