Skip to content

feat: rolling-window confirmations - #71

Open
karim-en wants to merge 9 commits into
devfrom
window-confirmation
Open

feat: rolling-window confirmations#71
karim-en wants to merge 9 commits into
devfrom
window-confirmation

Conversation

@karim-en

Copy link
Copy Markdown
Collaborator

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 × cap instead of cap.

The check is now: bridging an amount from a block at depth d must keep every
window [tip − k + 1, tip] with k ≥ d
within the tier that k
confirmations buy. Reading the tier table as a rate limit:

tier(A) = c means no more than A may be bridged out of any c-block span.

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   = max_tier_confirmations + confirmations_delta
             + extra_msg_confirmations_delta + 5
max_window = max_tier_confirmations + delta

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).

block bridged recorded in the pre-raise ring?
1001 6 BTC no — slot reused by block 1018
1020 6 BTC yes

A new 4 BTC deposit verified at 25 confirmations:

  • counted: 6 + 4 = 10 BTC → 4-conf tier → 25 confirmations is ample → accepted
  • actual exposure to a 25-block reorg: 6 + 6 + 4 = 16 BTC → past the 15 BTC
    line → the table wanted 30 confirmations → should have been rejected

Pinned by test_ring_grow_undercounts_an_evicted_block.

Impact

  • Bounded by roughly one window's worth of the affected tier's cap.
  • Self-healing: once max_window fresh blocks have passed, every height in every
    window was recorded under the new capacity. ~5 hours at a 30-block BTC window.
  • Requires a DAO tier change to open, and the direction is uncomfortable — the
    gap appears precisely when governance is trying to be more conservative.
  • The previous implementation failed safe here (an evicted block forced max-tier
    confirmations); this one fails open.

Options

  1. Document and accept. DAO-gated, bounded, self-healing.
  2. Reliability watermark. Store the highest height ever recorded; on a grow,
    set reliable_from = highest_seen + 1 − old_capacity and require max-tier
    confirmations for any window reaching below it. Costs one u64 of state, and
    for max_window blocks after a tier raise, deposits shallower than the max
    tier 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_window blocks
following 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 − 1 neighbouring blocks. Previously an
attacker 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.

tip event
1002 Bob's relayer verifies his 2 BTC from block 1001 — the window holds only his 2 BTC → clears at 2 confirmations
1002 Alice's relayer tries hers — the 3-block window [1000–1002] now holds 2 + 2 = 4 BTC, over the 3 BTC line → needs 4 confirmations → rejected
1003 Alice retries at depth 4 → 4 BTC is a 4-conf amount, 4 ≤ 4 → accepted

Alice waited two extra blocks because someone else spent the shared allowance in
a different block.

Impact

  • No loss of funds. The worst case is waiting max_window confirmations
    instead of the deposit's own tier.
  • No deposit can be starved. Every amount clears unconditionally at
    max_tier_confirmations + delta deep, because the widest window is satisfied
    by every amount the table can rate.
  • Not free to do deliberately. A griefer must bridge real value of their own
    and pay the miner fee, and their deposit consumes the same allowance they are
    denying.
  • Happens without an adversary. Ordinary busy traffic produces the identical
    effect. This is the rate limit working as designed, not a defect.

Guidance for relayers and integrators

  • Do not infer required confirmations from the amount. Call
    get_required_confirmations(block_height, amount, relayer_account_id, has_extra_msg); it returns the minimum depth that currently clears every
    window.
  • Re-quote before submitting. The answer is a snapshot: it can rise if
    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 amount failure as retryable, not terminal.
  • Expect higher confirmation counts at busy times, especially for deposits
    that would otherwise sit in the lowest tier.
  • Whitelisted relayers still avoid confirmations_delta, which both lowers the
    requirement 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:

binds at depth constraint
2–3 any 3-block window ≤ 3 BTC
4–9 any 9-block window ≤ 10 BTC
10+ unconstrained

≈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.

@olga24912
olga24912 marked this pull request as ready for review August 26, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants