Skip to content

Wait on the ripening schedule, not a clock, so an exit lands as one UTXO - #192

Closed
CypherBoxLLC wants to merge 1 commit into
fix/ark-exit-claim-batchingfrom
fix/ark-exit-claim-schedule
Closed

CypherBoxLLC wants to merge 1 commit into
fix/ark-exit-claim-batchingfrom
fix/ark-exit-claim-schedule

Conversation

@CypherBoxLLC

Copy link
Copy Markdown
Owner

Stacked on #181.

One exit should arrive as one UTXO. It does not.

drainExits sweeps every claimable capsule into a single transaction, so the whole exit can land as one UTXO at the destination. The last mainnet exit delivered 2,961 sats as five separate UTXOs.

#181 fixed the obvious half of that by holding the sweep instead of claiming the moment the first capsule ripens. The ceiling it holds against is wall-clock, and that is where it still breaks.

The three capsules still in flight on that exit reported:

05ec3efc  claimableHeight 963101
c51eb209  claimableHeight 963142
4b4e567e  claimableHeight 963145

44 blocks, about 7.3 hours. Against EXIT_CLAIM_BATCH_MAX_WAIT_MS = 6h:

Window opens (first capsule ripe) 963101
Window expires, 6h later ~963137
Capsules claimable at that point 1 of 3
Last capsule ripens 963145, ~80 min after the window closed

So it fires window-expired, sweeps one capsule, and the exit splits into two UTXOs. No ceiling tuned in hours fixes this in general: block intervals are stochastic, and the spread is a property of when each branch happened to confirm, not of how long anyone waited.

Blocks are the truth, time is the estimate

ExitState.AwaitingDelta.inner.claimableHeight is populated the moment a leaf confirms, and it does not move. The full schedule is known long before it matters, so "has everything ripened" is a question about the chain tip, not about elapsed time.

decideExitClaimBatch now holds until the tip reaches the last known claimableHeight. That wait provably terminates: the heights are fixed and the chain only goes forward.

The wall-clock backstop stays for the case it is actually good for, and only that case:

  • a straggler with no known ripening height (still Processing, no leaf confirmed yet)
  • an unreadable chain tip
  • the tip has passed every scheduled height and the stragglers still are not claimable, which means wedged rather than slow

A partial schedule does not count. Knowing two heights of three proves nothing about the third, so that falls back to the clock too.

The decision also returns blocksUntilAllReady, which feeds an honest "2 blocks to go" rather than a seconds countdown that visibly stalls and jumps, per spec 7.2.

Testing

21 unit tests, built on the three measured heights above. Mutation-checked:

Restored behaviour Tests failing
blind wall-clock ceiling (#181 as-is) 3
min instead of max of the ripening heights 2
wait on a schedule while a straggler is unscheduled 1
hold forever once the tip passes the schedule 1

Full unit suite 37 suites / 272 tests. tsc unchanged at the 405 baseline.

Relationship to #191

Independent, and they compound. #191 keeps capsules out of the exit set that would never have become claimable, so there are fewer stragglers to wait on and no dust exit that could hold the batch to its backstop. This one makes the remaining set arrive together.

…s as one UTXO

drainExits sweeps every claimable capsule into ONE transaction, so a whole
exit can arrive as a single UTXO. It did not: the last mainnet exit
delivered 2,961 sats as five separate UTXOs.

Batching held on a 6-hour wall-clock ceiling. The three capsules still in
flight on that exit reported claimableHeight 963101, 963142 and 963145,
a 44-block spread of about 7.3 hours, so the window expired roughly 80
minutes BEFORE the last one ripened, with one of three claimable. The
sweep fired early and split the exit in two. No ceiling tuned in hours
fixes this in general: block intervals are stochastic and the spread is
a property of when each branch confirmed.

The schedule is knowable in advance. AwaitingDelta carries the exact
block each straggler becomes claimable at, fixed from the moment its leaf
confirmed. So hold until the tip reaches the last of them. That wait
provably terminates, because the heights do not move and the chain only
goes forward.

The wall-clock backstop stays for the case it is good for: a straggler
with no known ripening height, still broadcasting, or an unreadable
chain tip. There is no schedule to wait on there and something has to
bound it. The wait also stops if the tip passes every scheduled height
and the stragglers still are not claimable, which means wedged rather
than slow.

The decision now also reports blocks remaining, so the wait can be shown
as "2 blocks to go" rather than a seconds countdown that stalls and
jumps.

21 unit tests over the measured heights. Mutation-checked: restoring the
blind ceiling fails 3, taking min instead of max fails 2, waiting on a
partial schedule fails 1, holding forever past the schedule fails 1.
@CypherBoxLLC

Copy link
Copy Markdown
Owner Author

Landed in main via #208, which merged the device-tested integration branch as a unit.

This PR did not auto-close because its base was another feature branch rather than main, so GitHub never evaluated it against main. Retargeting is refused with "There are no new commits between base branch 'main' and head branch", which confirms the commits are already reachable from main.

Closing as merged.

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.

1 participant