Repository navigation
fix: hold a redo spark exit rebuild over a waiting leaf - #1309
Conversation
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 320f0b95bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
[Medium impact] The PR appears safe to merge; no actionable new issues were found. SummaryThe PR holds incomplete
Tests were reviewed but not run. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Redo rebuild] --> B{Leaves skipped but present in old recovery?}
B -->|No| C[Adopt rebuild]
B -->|Yes| D{Six blocks elapsed?}
D -->|No or tip unreadable| E[Keep old record without broadcasting]
E --> F[Wait for rebuild interval]
F --> A
D -->|Yes| G[Continue with normal waiting-leaf rules]
Reviews (3) · Last reviewed commit: "fix: redo hold keeps the rebuild pace af..." · Reviewed by Greptile |
coreyphillips
left a comment
There was a problem hiding this comment.
Two independent reviews, nothing blocking a merge.
worth doing, does not block
- Redo hold after adopted top-up rebuilds every tick and shows a ₿0 fee ask (Bitkit/Services/SparkExit/SparkExitDriver.swift:1008-1021)
- A funded Redo hold retains the funding phase (Bitkit/Services/SparkExit/SparkExitDriver.swift:1018-1020)
- An empty prepare extends the bounded Redo hold (Bitkit/Services/SparkExit/SparkExitDriver.swift:477-478)
nits
- Lost-race Redo test does not check which funding the rebuild spends (BitkitTests/SparkExitTests.swift:1387-1426)
|
Milestone 3.2.0: this PR is the iOS twin of synonymdev/bitkit-android#1892 and fixes the Spark emergency exit, which ships in 3.2.0 with #1128. It closes no issue, so the milestone sits on this PR. |
talosmachina
left a comment
There was a problem hiding this comment.
No findings. Same hold as synonymdev/bitkit-android#1892: a Redo rebuild that leaves out a waiting leaf the diverged generation recovers stays rebuilding for redoHoldBlocks instead of being adopted. Reviewed 320f0b9 against feat/breez-spark-lightning, full tier, static: iOS does not build here, so this is reasoned from the code and CI.
What I checked, and 4 candidates I ruled out
Read: SparkExitDriver.build in full (both holdsRedo sites, the failed-only and full-build adopt paths, unexitable and lost-leaf exits), tick's .running where record.isRebuilding branch, SparkExitRecord.adopt, the new tests in SparkExitTests.swift
Twin: logic matches the Android PR line for line (6 blocks, saturatingSub where Android guards height >= since)
Ruled out
- Stale
redoHeldSincereaching a later Redo:adoptclears it, andrebuildStalledand the unverified/fee-bump rebuilds only start from arunningcheck, which never runs while rebuilding. - Decoding old records:
redoHeldSinceis an optional on the synthesizedCodablerecord, so records written before this change decode with nil. - Rebuilding every tick while held:
lastBuildAt = now()keepsisRebuildDuepacing it. - Hold without a tip never ends: by design and commented; it starts counting at the first tip it reads.
CI: SwiftFormat and Greptile green; Run Tests still running at review time.
Merge confidence: 4/5, Run Tests had not finished and the change was not built here.
|
e11efd3 addresses @coreyphillips's review:
Two new tests cover the first two. The Spark exit suites and SwiftFormat lint pass locally. Re-review is needed only because of this push: 320f0b9...e11efd3 |
jvsena42
left a comment
There was a problem hiding this comment.
No findings. holdsRedo uses the same predicate as Android synonymdev/bitkit-android#1892:
saturatingSubkeeps the hold through a reorg.- The tip and the SDK share
Env.sparkChainServiceUrl(.bitcoin)on mainnet, so a hold with no readable tip isn't reachable while the SDK still reads. payHigherFeere-checksphase == .running, !isRebuildingbeforeapplyFeeBump(:861), so a held record can't be fee-bumped.- Strikes are recomputed from the stored ones on each held build and saved only on adopt.
Device gate: no device check needed: needs a lost CPFP race; the driver tests cover it. perf: about 6 extra prepare/recover rounds per Redo episode.
Reviewer model: Claude Fable 5.2 (claude-fable-5-2), Claude Opus 5.5 (claude-opus-5-5)
Reasoning effort: not exposed
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after 1 new commit (e11efd3). Nothing blocking remains.
addressed
- Redo hold after adopted top-up rebuilds every tick and shows a ₿0 fee ask (Bitkit/Services/SparkExit/SparkExitDriver.swift:1008-1021). Fixed:
holdsRedonow sets the phase to.runningoncefundingShortfallSatsis nil (SparkExitDriver.swift:1025-1028). The next tick therefore takes the.running where record.isRebuildingbranch at :182-185, which is gated byisRebuildDueon thelastBuildAtset at :1024. In.running,remindersgets past :319 and plans the-rebuildreminder at SparkExitFunding.swift:330-332, and the screen no longer shows the needs-funding copy. I rantestARedoHoldAfterAFundedTopUpRunsAtTheRebuildPaceon an iPhone 17e simulator at e11efd3 and it passes. Itsphase == .runningassertion would fail without the change. - Lost-race Redo test does not check which funding the rebuild spends (BitkitTests/SparkExitTests.swift:1387-1426). Fixed: The doc comment at SparkExitTests.swift:1387-1388 now claims only the single strike, the sent refund and no lost leaf, and :1418-1426 assert each of those. The QA note was trimmed the same way. The fixture comment at :1407 ("spends the funding that is still there") still describes funding, but it explains the setup rather than claiming coverage.
- A funded Redo hold retains the funding phase (Bitkit/Services/SparkExit/SparkExitDriver.swift:1018-1020). Fixed: This is the same change as point 1, at SparkExitDriver.swift:1025-1028. A Redo hold whose top-up was adopted leaves
.needsFunding, so theneedsFundingelsebranch at :174-175 no longer builds on every tick.isAwaitingOwnPaymentno longer matters for thefunding-reminder, because.runningnever plans one. When a shortfall is still open (isRedoFromHeldFunding), the phase stays.needsFundingand :130-131 paces the build withisRebuildDue. The new test passes locally. - An empty prepare extends the bounded Redo hold (Bitkit/Services/SparkExit/SparkExitDriver.swift:477-478). Fixed: In the empty-prepare branch,
holdsRedonow runs on the provisional skips beforeretrySince/retriedAtare set (SparkExitDriver.swift:391-395). During the hold the build is paced bylastBuildAtand is not gated by the guard at :378-379. The retry clock starts only onceredoHeldSinceisredoHoldBlocksold.testAnEmptyPrepareDuringARedoHoldLeavesTheRetryClockAlonechecks both halves and passes at e11efd3. All 89SparkExitDriverTestspass.
The base branch was changed.
{"kind":"bitkit-retarget-execution-v1","repository":"synonymdev/bitkit-ios","pr":1309,"generation":"43261276958-1","sourceSha":"96225a120f3e60673506f483d78237fd8cea485e","sourceRef":"master","owner":"github-actions[bot]"}
|
#1128 merged into @coreyphillips @jvsena42 your approvals were on this same head (e11efd3), but |
|
The retarget e2e run stopped at its merge-checkout check: the checked-out merge no longer matched the PR base after the retarget to |
{"kind":"bitkit-retarget-execution-v1","repository":"synonymdev/bitkit-ios","pr":1309,"generation":"43261782315-1","sourceSha":"96225a120f3e60673506f483d78237fd8cea485e","sourceRef":"96225a120f3e60673506f483d78237fd8cea485e","owner":"github-actions[bot]"}
coreyphillips
left a comment
There was a problem hiding this comment.
Rechecked after replies, with no new commit since e11efd3. Nothing blocking remains.
|
Restarted the retarget E2E with a fresh dispatch: the earlier run and its rerun both stopped at the checkout check, because master moved after the merge sha was taken. No test had run. |
{"kind":"bitkit-retarget-execution-v1","repository":"synonymdev/bitkit-ios","pr":1309,"generation":"43265867784-1","sourceSha":"95a29d0946783ed0504ab77905c6930e12da38b9","sourceRef":"95a29d0946783ed0504ab77905c6930e12da38b9","owner":"github-actions[bot]"}
|
Closing and reopening this PR to make GitHub rebuild its merge commit. It still merged onto the old Spark base (bbf02ca) after the retarget to master, so the retarget E2E stopped at its checkout check every time. Approvals and code stay as they are. |
{"kind":"bitkit-retarget-execution-v1","repository":"synonymdev/bitkit-ios","pr":1309,"generation":"43267072908-1","sourceSha":"95a29d0946783ed0504ab77905c6930e12da38b9","sourceRef":"95a29d0946783ed0504ab77905c6930e12da38b9","owner":"github-actions[bot]"}
|
Reopened this PR twice more to clear stale checks. The runs the first reopen started had checked out the old merge commit and failed their checkout check, and a rerun repeats that commit. So the first reopen rebuilt the merge commit on current master (21e87d6), and the second started fresh E2E and integration runs on it. Code and approvals are unchanged. |
|
Merging: approved on this head, required checks green after the merge ref was rebuilt on current master, no open threads. |
Twin: synonymdev/bitkit-android#1892
Description
check_recover_fundsasRedo, notValid. The driver never sends a diverged generation again, and its Redo rebuild skipped the hold that keeps a leaf in the exit when a build leaves it waiting. So when one failed chain read made the rebuild's prepare skip a leaf asUnverified, that leaf's steps went unsent until the rest of the exit was done, which can be weeks. Before 0.28.2 the same race went through the lost-race fallback rebuild, which kept that hold.Validstays as it is.Design
N/A — no UI changes.
Preview
N/A
QA Notes
Automated Checks
SparkExitTests:testARedoRebuildBesideAWaitingLeafHoldsUntilTheLeafReads: a Redo rebuild whose prepare skips leaf-2 asUnverifiedadopts nothing and sends nothing, then adopts both leaves once leaf-2 reads.testARedoRebuildBesideALeafThatStaysUnreadAdoptsAfterTheHold: the hold keeps its start height for 6 blocks, then the rebuild is adopted with leaf-2 waiting.testARedoHoldAfterAFundedTopUpRunsAtTheRebuildPace: a hold whose top-up was adopted leaves the funding phase and doesn't build again before the rebuild interval.testAnEmptyPrepareDuringARedoHoldLeavesTheRetryClockAlone: a prepare that skips every leaf during the hold sets no retry clock; it starts once the hold ends.testARedoAfterALostFeeRaceStrikesTheSpentRefundOnlyOnce: a Redo over a refund logged as input-spent strikes the refund once; after the rebuild's refund is accepted, a second Redo does not strike it again. Asked for by @jvsena42 on chore: bump breez sdk to 0.28.2 #1306 (review).testReadsARecordWithoutTheNewFieldsAndRoundTripsThem: an older record readsredoHeldSinceas nil, and a set value survives a round trip.SparkExitReviewTests,SparkExitSweepTests,SparkExitFeeBumpTests,SparkExitRerunTests) pass locally on a simulator.