Skip to content

feat: spark emergency exit - #1217

Draft
ovitrif wants to merge 3 commits into
feat/breez-spark-lightningfrom
feat/spark-emergency-exit
Draft

ovitrif wants to merge 3 commits into
feat/breez-spark-lightningfrom
feat/spark-emergency-exit

Conversation

@ovitrif

@ovitrif ovitrif commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Twin: synonymdev/bitkit-android#1782
Stacked on #1128

This PR adds Emergency exit, Spark's unilateral exit, so users can move their Spark balance to Savings on-chain without the Spark operators. It builds on the Spark wallet in #1128, which already backs up Spark's exit data to VSS as SPARK_EXIT.

Description

Screens and flow

  • Spark detail › Emergency exit opens six screens:
    • an explainer: last resort, up to 4 weeks, on-chain fees, small amounts can stay behind, and it runs in the background;
    • a quote from the SDK's prepareRecoverFunds at its fastest recommended rate: amount, network fees, what arrives, fee funding, what stays on Spark. Continue is off when nothing is recoverable or the fees exceed the value;
    • funding: pay the CPFP fee funding from Savings, or send it from another wallet to the shown address and QR;
    • a swipe-to-confirm;
    • progress, with the steps and the timelock countdown;
    • the result: done, more fees needed, or stopped.
  • The exit is a SPARK_EXIT transfer to Savings: Home and Savings show it in transfer with the countdown from its timelock, settled when the sweep reaches Savings. While it runs, Spark neither sends nor receives Lightning, Spark reconnects with leaf optimization off so no swap hands an exiting leaf to someone else, and its balance counts only as in transfer. The result lists what stayed on Spark: leaves too small to exit and cooperative leaves the operators did not sign.
  • Until the leftover sweep confirms or the leftover is dust, Emergency exit and the Spark row open the ended exit's result instead of a new exit; the row shows even while Spark is not ready.
  • The quote, funding and confirm views scroll above their bottom buttons, so headlines stay whole on 375 pt screens, and their notes use body text instead of the all-caps caption style.
  • A stopped exit says only "Nothing could be recovered.", adding what Spark still holds beyond the exit's leaves, since lost leaves are gone; a stopped exit with lost leaves shows the red lost line under its steps.

Building and broadcasting

  • The fee funding key derives from the wallet seed with bitkit-core on m/84'/{coin}'/7797'/0/{i}, with a fresh index for each exit kept in its record (older records use 0), an account no Bitkit wallet uses, so ldk-node never spends the funding UTXO or the CPFP change chained to it; singleKeyCpfpSigner signs with it. The exit builds only once the funding UTXO confirms, read from the SDK's own chain host.
  • The SDK never broadcasts, so the app does: a step with a CPFP child goes to mempool's POST /api/v1/txs/package as a JSON array, others to POST /api/tx, on the hosts the SDK reads from (mempool.space then blockstream.info on mainnet, Spark's regtest mempool on dev). Node answers are classified: already known counts as accepted, missing inputs and non-final are retried, anything else is shown after 3 refusals.
  • The signed recovery is stored in spark-sdk's own serde JSON shape inside a versioned record (request, funding, generation, broadcast log), saved before anything is broadcast, so the Breez CLI's check-recover-funds reads it too. A Redo verdict is saved first and rebuilds a new generation; the old one is never sent again.
  • When leaves share tree nodes with another wallet exiting at the same time, the other wallet's CPFP child can confirm a node first. Our child then never confirms, and the next step's child, which spends our child's change, is refused every tick (min relay fee not met, 0 < 17) while checkRecoverFunds still answers Valid. Before broadcasting, the exit looks up each of its children that a pending step builds on and whose node is confirmed. When no chain host has that child, the exit rebuilds from the same funding and never sends the dead chain again. As a fallback, a step whose parents are confirmed and whose fee chain fails 3 times in a row (min relay fee not met, 0 < or missing inputs) rebuilds the exit once, and those failures don't count as refusals until then.
  • Before each SDK check, the steps whose parents are confirmed and which the chain host shows in a block are marked confirmed. The SDK only asks the chain about a step the caller already saw confirmed, and Spark's regtest mempool does not report spenders, so without this the exit stalled after its fan-out.
  • Without the SDK, a step is sent only once each input's relative timelock has matured, counted from the confirmation height of the parent it spends; non-BIP68-final is never counted as a refusal. A step whose own input another transaction spent is not sent again until the next SDK check rebuilds.
  • Before the quote, the exit waits for a leaf optimization in flight. After one was cut short, it also waits until the swap's counter-swap is past the SDK's 5-minute claim grace, then syncs, so value the swap still owes the wallet is in the quote. The swipe waits for an optimization in flight before it reconnects Spark. While the exit runs, Progress shows any Spark balance beyond the exit's leaves.
  • Every load settles exit transfers that no running exit owns (a record set aside as unreadable, or a crash before the transfer id was saved), so their amount never stays in transfer to Savings.

Background run

  • Runs the exit in the background, so users don't have to open Bitkit until it finishes. A driver ticks every minute while Bitkit is open. While it is closed, a BGAppRefreshTask (to.bitkit.spark-exit, 25 s budget, earliest 15 minutes) and a BGProcessingTask that needs the network (to.bitkit.spark-exit.processing, 4 minute budget, earliest 30 minutes) run the same step from a cold launch with no UI: load the record, send the stored, already signed steps whose parents confirmed without the SDK, start the Spark SDK alone (never ldk-node) within the time left, then check, sign a build or rebuild, broadcast, save and resubmit both tasks. Info.plist permits both identifiers and the fetch and processing background modes.
  • When the OS expires a run, it stops before its next SDK call or broadcast; every broadcast is saved as soon as its answer arrives, so the record is always whole and the next run does not resend what was accepted.
  • Measured against Spark's hosted regtest over a stored 30-step exit, from a cold start: the whole run took 1.26 s against the 25 s refresh budget, and the process footprint grew by about 2 MB. Chain status reads run eight at a time, so the pass over 30 steps takes about 1 s instead of 7.

Notifications

  • Local notifications are the fallback when no background run gets to a step (Background App Refresh off, Low Power Mode, no network). They are scheduled ahead on every run: at each unpublished step's maturity plus 12 blocks, inside the operators' 50-block window; after the same grace for a step ready to publish; 4 hours after the exit last moved while steps wait for parents; and at once when the exit needs more fee funding or a step is refused 3 times. A run that publishes a step, or sees it confirm, removes its notification before it fires. Confirm asks for notification permission before the swipe.
  • Confirm and Progress show a note while Background App Refresh is off, Low Power Mode is on or notifications are denied, saying what that changes.
  • A failed or partly lost exit keeps its result and notifies until the user opens it; a fully lost exit notifies once.
  • While a rebuild is pending, the replaced generation's step reminders give way to one reminder past the rebuild's due time, so an exit that ends lost on that rebuild posts only its lost notification.

Funding and the leftover sweep

  • A top-up asks only for what the funding inputs lack. InsufficientCpfpFunds names the total all inputs must hold, so the shortfall is that total less what the inputs hold at the funding address now: the unspent inputs and the CPFP change chained from them, this generation's and earlier ones'. Several small payments that together cover it are all adopted, and Payment seen counts only new funding. In a regtest run, the exit had asked for 9,085 more when 50 were missing.
  • Once Bitkit paid the fee funding from Savings, or a payment covering it is seen unconfirmed, Progress shows it as paid or seen with its transaction ID and no QR, and asks only for what is still missing after it confirms. The fee funding paid from Savings shows in Activity as an ordinary payment, not a transfer to Spending.
  • When an exit ends, the fee funding left at the funding address goes to Savings in one transaction, signed with the funding key at the exit's fee rate. That leftover is CPFP change, or funding a rebuild did not need. The sweep runs in the foreground and in background runs. A leftover under the dust limit after the fee stays, and the result shows it with its address, and the sweep as sending, sent with its transaction ID, or too small and kept.
  • A failed exit claims only what stays on Spark.

Lost leaves and the fee ladder

  • A leaf whose refund two rebuilds in a row reproduce after its input was spent was claimed on-chain by an older owner of its funds: it leaves the exit, which goes on with the rest or ends as stopped, and Progress shows and notifies the amount.
  • The quote says how much of the Spark balance it cannot exit: the skipped leaves plus anything beyond the leaves the SDK lists, such as a counter-swap still unclaimed.
  • SDK facts the exit relies on (spark-sdk with SDK 0.27.2):
    • The first quote uses prepareRecoverFunds with All, which can't abort: a leaf that can't pay its refund's fee at the chosen rate, such as a fee-ladder leaf, is listed under skipped as FeeExceedsValue and stays on Spark.
    • A rebuild uses Specific with the exit's leaves, which aborts as a whole when one anchorless fee-ladder leaf can't pay its own refund, with an untyped SparkError naming the leaf. The app drops that leaf from the exit and prepares again, and falls back to RecoverableOnly when the error names no leaf.
    • Mainnet wallets mostly hold fee-ladder leaves, so a rebuild at a higher fee rate can leave several of them behind.

Out of Scope

  • Emergency exit: per-branch funding, and a fee bump of a built exit.
  • Backing up the exit record itself: a wallet restored on a new device starts the funding index at 0 again and does not look at the funding addresses of earlier exits.
  • SPARK_EXIT exit data backup: in feat: add spark via breez sdk #1128.

Risks for review

  • Emergency exit in the background. iOS decides when background tasks run and grants none while Background App Refresh is off or Low Power Mode is on. About 50 blocks (8 hours) after a step matures, the operators can publish their own version of it, paid from the leaf, so a step no run publishes in time is reminded 12 blocks after it matures. The reminders need notification permission; without it, only background runs and opening the app drive the exit, and Progress says so. The SDK documents its All selection, which the exit uses so it can take leaves still in the balance, for when the operators are unreachable or refuse to serve; with healthy operators, Transfer to Savings stays the cheaper way out, and the explainer says so.
  • Emergency exit cost. At 2 sat/vB every leaf under about 4,096 sats costs more to exit than it holds and stays on Spark, so a typical balance recovers about 75% after fees, and an exit takes about 1,900 blocks (13 to 14 days) from the first broadcast to the sweep.

Design

N/A — no design available.

Preview

Emergency exit on aa01843 against Spark's hosted regtest, with notifications denied. Pay from savings: explainer, the quote naming the 9,789 sats it can't exit, Pay from savings, the permission prompt, Confirm with the notifications note, then Progress showing the payment as paid with its transaction ID and no QR, and Cancel exit (on dev that payment goes to Bitkit's staging chain, so it never confirms on Spark's). Fund from another wallet: the faucet pays the funding address, Payment seen, then Progress waits for it with its transaction ID until it confirms, and the exit builds 28 transactions and starts publishing; the wait for the faucet is cut.

Pay from savings Fund from another wallet
e1-exit-start2x.mp4
e2-cut2x.mp4

A full exit at 030de21 on the local Spark regtest stack (iOS 26.5): the start, paid from Savings, and the end, where the timelock counts down and the sweep lands 91,288 sats in Savings. This build predates the fixes above, so after paying it still shows the QR.

Start Timelock and sweep
q1-exit-start2x.mp4
q2-exit-end2x.mp4

UI pass on an iPhone SE (375 pt), rendered with stubbed state by a local hook that is not in this PR: the Start exit? headline was cut to one line, and the exit quote's note was in the all-caps caption style. Content now scrolls above the buttons, and the notes read as body text.

Start exit?, before Start exit? Exit quote, before Exit quote

The funding address view scrolls its QR on the SE, and Start exit? on an iPhone 17.

Funding address, SE Start exit?, iPhone 17

Screens

Every exit screen, on an iPhone 17 simulator in Bitkit's dark theme, rendered with stubbed state by a local hook that is not in this PR.

Intro Quote with the can't-exit note Funding choices Funding address
Partial payment hint Notification permission Confirm Progress: fee funding asked
Progress: paid from savings Progress: payment seen Progress: running More fees needed
Done Done, leftover swept Stopped Stopped, leaves lost

QA Notes

Setup

  • Dev build (regtest) needs no key. Spark is funded from Lightspark's regtest faucet, sent to the deposit address on the Spark screen (Settings › Advanced › Lightning Connections › Spark). On dev, an exit's Pay from savings goes to Bitkit's staging chain and never confirms on Spark's; Fund from another wallet with the faucet does.
  • A full exit needs a chain you can mine: Lightspark's hosted regtest takes the real CSV of about 1,900 blocks, and its faucet needs credentials only Breez's CI holds. The exit results below ran on a local Spark regtest stack built from spark-sdk's public regtest/local setup (the last public operator), with bitcoind, 3 operators, the SSP, electrs and mempool, mined on demand. Two patches make SDK 0.27.2 work with that operator: the operator skips the RETURNED status filter it does not know, and the wallet turns fee ladders off, so every leaf there is an anchored leaf.
  • Besides the app, a small harness on SDK 0.27.2 made the same calls as the app's driver (prepareRecoverFunds with All, then Specific on a rebuild, recoverFunds with a single-key CPFP signer, checkRecoverFunds, package broadcast through mempool), each step a separate process resuming from storage.

Journeys

N/A — iOS has no exit journey; the exit needs Spark's regtest faucet and hours of blocks, which the journey environment lacks.

Manual Tests

  • 1. Dev, Spark funded from the faucet, Settings › Advanced › Lightning Connections › Spark › Emergency exit → explainer, then the quote lists amount, network fees, You receive about, Fee funding needed and what stays on Spark — the faucet is outside the journey environment.
  • 2. Dev, continue with Fund from another wallet, send the fee funding from the faucet to the shown address, swipe → progress shows the funding step done after 1 confirmation, then publishing, the timelock countdown and the sweep; the exit survives an app kill and continues on reopen — faucet and hours of regtest blocks.
  • 3. Mainnet, small Spark balance, Emergency exit paid from Savings → Home shows the amount in transfer with a countdown, Spark stops sending and receiving, and the amount lands in Savings at the end — mainnet funds over days.
  • 4. Phone under Xcode, dev, an exit running: background Bitkit, pause in the debugger and run e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"to.bitkit.spark-exit"], continue → the log shows Spark exit background run: connected=true with the time per step; repeat with to.bitkit.spark-exit.processing, then once more followed by _simulateExpirationForTaskWithIdentifier: → the run stops, and the next run sends nothing already accepted. Turn Background App Refresh off for Bitkit → Progress shows the note; Low Power Mode on → its note — the simulator has no BGTaskScheduler.
  • 5. Dev, Spark funded, Emergency exit with Pay from savings → Progress shows "Paid from savings, waiting for confirmation" with its transaction ID and no QR or ask; Cancel exit backs out — Spark's regtest is not in the journey environment.
  • 6. Dev, Emergency exit with Fund from another wallet, pay the address from the faucet → Funding shows Payment seen, Progress shows "Payment seen, waiting for confirmation" with its transaction ID until it confirms, then publishing — faucet.
  • 7. Local Spark regtest, an exit that ends after a Redo needing no funding → Emergency exit and the Spark row open the result with the leftover sweep sending, then sent with its transaction ID; a new intro shows only after it confirms — needs a chain you can mine.
  • 8. Local Spark regtest, a second exit on the same wallet → its funding address differs from the first exit's — same.
  • 9. Local Spark regtest, kill Bitkit while refunds wait for their timelock, mine to maturity and relaunch → nothing is sent before it matures, and non-BIP68-final never counts toward the 3-refusal alert — same.

Automated Checks

  • added SparkExitTests.swift — the background entry point from a cold start sends stored steps and then checks with the SDK only, signs a build once funding confirmed, stays keyless when Spark does not connect or time is short, and after an expiry mid-broadcast leaves a whole record from which the next run sends nothing twice; reminders are planned at each unpublished timelock plus the grace, dropped once a step is published, sent at once for fee funding and repeated refusals, and after a stall while steps wait for parents; the record round-trips with every status, error and input variant; the recovery is written in spark-sdk's serde shape and a CLI-written one reads back; an unknown version is refused; the driver waits for a confirmed funding UTXO, saves before broadcasting, broadcasts ready steps once until due, surfaces repeated refusals, finishes on Done with the leftover read, rebuilds a new generation on Redo after saving it, asks only for what the inputs lack on InsufficientCpfpFunds, adopts several small payments together and ignores CPFP change at the funding address, resumes after a restart without repeating broadcasts, retries cooperative leaves the operators could not sign, and broadcasts without the SDK only steps whose parents confirmed and timelocks matured; a leaf whose refund two rebuilds reproduce after its input was spent is lost, alone or with every leaf (the exit stops); a rebuild leaves behind a leaf that cannot pay its refund and retries, falls back to RecoverableOnly when the error names no leaf, and stops when none is left; the quote math, funding states, funding key path, the package and single broadcasts, path and host fallback, and the node answer classifier
  • added SparkExitSweepTests.swift — the leftover sweep spends every UTXO at the funding address to Savings less its fee, each input's signature verifies against its BIP143 digest with libsecp256k1, a leftover under dust after the fee is skipped, output scripts for every address type, an ended exit keeps its result reachable until the sweep confirms, and a background run sweeps without starting Spark
  • added SparkExitRegtestTests.swift — opt-in (TEST_RUNNER_SPARK_EXIT_REGTEST=1): a fresh wallet on Spark's hosted regtest connects and quotes an empty exit, the chain host serves the tip and funding UTXOs, a fake parent and child package reaches the live node and is read as retry later, and the background entry point runs from a cold start over an exit of the live run's shape
  • SparkExitTests.swift also covers a fee chain that lost a race, with the operators down and the check answering Valid (rebuilt from the same funding, the dead chain never resent), a child still in the mempool, the safety net's single rebuild before the step's refusals surface, the chain host's not-found lookup, and the needs-funding amount never under the 294-sat dust limit, and a replay of a single-leaf exit an older state claimed, which across its rebuilds delivers only the lost notification
  • updated SparkConfigTests.swift — leaf optimization is on, and off while an exit runs
  • ran the unit suite (CI's selection) on this head and on feat: add spark via breez sdk #1128's head; SwiftFormat and the translation check pass

Test results

QA:
Tested on an iPhone 17 Pro simulator (iOS 27.0) against Spark's hosted regtest, and on an iPhone 17 Pro simulator (iOS 26.5) against a local Spark regtest stack (QA Notes, Setup).

Device tests skipped ⚠️:

  • Background runs: the simulator has no BGTaskScheduler (4).
  • Mainnet, a fee-ladder leaf exit and the leftover sweep on a funded run: see Open.
🟢 Exit paid from savings, Exit funded from another wallet
Exit paid from savings

1 and 5 at aa01843: the quote read 90,112 to exit, 7,969 in fees, about 82,143 received, 8,257 of fee funding and 9,789 staying on Spark, named as balance it can't exit; Pay from savings sent 8,679 from Savings, and Progress showed it as paid with its transaction ID and no QR, with the notifications note after permission was denied; recording under Preview.


Exit funded from another wallet

6, and 2 up to publishing, at aa01843: the faucet paid 8,257 to the funding address; Funding showed Payment seen, Progress waited for it with its transaction ID, and once it confirmed the exit built 28 transactions and published the first two; recording under Preview.


🟢 Full exit to Savings, Resume after a kill
Full exit to Savings

On the local stack at 030de21, with the operators running: a 100,000 sat Spark balance quoted 90,112 to exit, 4,855 in fees and 6,031 of fee funding; Pay from savings sent 6,453 (the funding plus its fee). The app published each refund once the 1,900-block timelock matured, before the operators' 1,950-block version, and sweep 02c28284… confirmed at height 9,246, paying 91,288 sats to Savings (90,112 − 4,855 + 6,031). The result read "Exit complete. Your funds are in savings. 18 small amounts stayed on Spark."; recordings under Preview. That build still asked to send the funding after paying it, which 169ff91 fixes.


Resume after a kill

Bitkit terminated at 6 of 22 transactions with one block mined while closed reconnected on relaunch and broadcast the next packages.


🟢 Rerun on the fixes, Failed exit
Rerun on the fixes

On the local stack at 674f0da, one simulator wallet, two exits, leaf optimization off in the test build:

Case Result
Quote names the balance it can't exit 🟢 "₿ 1,696 of your Spark balance can't be exited right now"
Top-up as two payments 🟢 2,000 then 2,348; Payment seen once the sum reached 4,348
Progress shows the balance outside the exit 🟢
Redo after the operators broadcast first 🟢 generation 2, sweep of 96,237 to Savings
Leftover sweep 🟢 1,394 and 978 back to Savings
Pay from savings shows paid, waiting for confirmation 🟢 with its transaction ID, no address or QR
New funding address per exit 🟢
An older owner claims a leaf 🟢 dropped after two identical rebuilds (about 10 minutes); the rest swept 65,437, and Done names the lost 8,192

Failed exit

At 41d8569: a single-leaf exit (32,768, received straight from a previous owner) with the operators down, where the previous owner's older refund won while the app was closed. About 16 minutes after relaunch the app dropped the leaf, swept the 626 leftover to Savings and showed two notifications. Emergency exit kept the result until it was opened once ("Exit stopped", "₿ 32,768 was claimed on-chain by an older owner of these funds and can't be recovered", and the leftover line); the next opening showed the fresh intro.


🟢 Operators stopped, Operators broadcast first, Funding short, Stale state
Headless runs through the harness, all at 2 sat/vB, each wallet funded with a 100,000 sat deposit (99,865 after the claim fee), which the SSP splits into 17 power-of-two leaves.

Operators stopped

With all 3 operators stopped, SDK 0.27.2 connected in 29 ms, quoted from local storage in 0.1 s and built 25 transactions (1 fan-out, 19 node packages, 4 refunds with a 1,900-block CSV, 1 sweep); every package was accepted. 4 leaves (90,112 sats) went out, 13 leaves (9,753 sats) were skipped as FeeExceedsValue, and 91,680 sats reached the destination (90,112 plus 1,568 unused funding through the sweep) over 1,961 blocks, about 13.6 days on mainnet.


Operators broadcast first

With the app asleep 60 blocks past maturity, the operators' watchtower broadcast its own refunds (1,950-block CSV, fee from the leaf); the next check returned Redo (OnChainStateDiverged), the Specific rebuild of the same 4 leaves needed no funding, and the exit reached Done with 85,749 sats. The 5,035 sats of first-generation change it left behind are what the leftover sweep now sends to Savings.


Funding short

With 9,035 sats over 5 UTXOs against a 8,541 quote, recoverFunds failed with InsufficientCpfpFunds asking for 9,085 in total; a 1,000 sat top-up was adopted with the earlier UTXOs, and the exit reached Done. The app now asks for the 50 missing instead of 9,085.


Stale state

A previous owner broadcast its older refund for a 16,384 sat leaf once it matured, 100 blocks after the current owner's, while the current owner slept, and swept it. The current owner's check returned Redo, and each rebuild reproduced refund be4def69…, refused as bad-txns-inputs-missingorspent. The app now drops such a leaf after two identical rebuilds (Rerun on the fixes). With the operators down, the 100 blocks after each maturity are the hard deadline, about 16 hours on mainnet.


Evidence

  • One notification for a lost exit, at 5d18a238: the exit tests and the unit tests (CI's selection) pass on an iPhone 16 simulator (iOS 27.0); SwiftFormat passes.
  • Fee chain that lost a race, and the dust floor on the needs-funding amount, at 46ae7556: the exit tests and the unit suite (CI's selection) pass on an iPhone 16 simulator (iOS 27.0); SwiftFormat passes.
  • Emergency exit fixes from the local regtest runs (result after Done, no double pay, a fresh funding address per exit, keyless timelocks, lost and unexitable leaves, the quote's unexitable amount) at 174d9ed7: the exit tests and the unit suite (CI's selection) pass on an iPhone 17 Pro simulator (iOS 27.0); SwiftFormat and the translation check pass.
  • Emergency exit funding, leftover sweep and late value at 022d3ed2: the exit tests and the unit suite pass on an iPhone 17 simulator (iOS 27.0).
  • Emergency exit in the background at 335182af: the unit suite and the exit tests pass on an iPhone 17 simulator (iOS 27.0). Against Spark's hosted regtest, the background entry point from a cold start, over a stored exit of 30 steps: keyless pass 1.03 s (7.00 s before status reads ran concurrently), SDK connect 0.03 s (before the SDK's own sync), checkRecoverFunds on one real step 0.55 s (0.17 s warm), one package broadcast to the live node 0.17 s, prepareRecoverFunds 0.55 s, whole run 1.26 s against a 25 s refresh budget; the process footprint grew by about 2 MB (25 to 27 MB, in a test host with no UI).
  • Emergency exit at f6d639cb: SparkExitRegtestTests passes against Spark's hosted regtest (empty-wallet quote, chain reads, a live package answer).

Open

  • Hosted-regtest run. Not run yet: a full exit on Spark's hosted regtest needs a funded regtest wallet and the real 1,900-block CSV.
  • A mainnet exit run. Not run yet. It is the only run with Lightspark's current operator code, mempool.space's package endpoint on mainnet and real block times: about 50,000 sats in a fresh wallet, at 2 to 4 sat/vB, over 13 to 14 days, with the app waking within about 50 blocks of each step maturing.
  • Fee-ladder leaves. Mainnet wallets hold fee-ladder leaves, whose refunds and exit path differ from the anchored leaves the local stack can build; no exit of a fee-ladder leaf has run.
  • BGTaskScheduler on a real device. The simulator has no BGTaskScheduler, so the OS granting and expiring the exit's tasks is not exercised (4).
  • Translations. The exit keys are English in every locale file; they need translating.
  • On dev, the Home countdown compares a Spark-chain timelock height with the staging chain's height, so it is wrong there; the progress screen uses Spark's chain.
  • The result after Done with its sweep, the fresh funding address, the keyless timelocks and lost or left-behind leaves are covered by unit tests only (7 to 9). A lost or left-behind leaf's value stays in the exit's in-transfer amount until the exit ends.
  • An optimization that starts while Spark connects is not seen, since the event listener is added after connect returns, so a quote right after launch does not wait for it.

Moves the Spark balance on-chain without the Spark operators: quote, fee funding from Savings or another wallet, swipe to start, then a driver that builds, broadcasts and checks the SDK's recovery until it lands in Savings, after restarts and from background tasks too. Lost leaves are given up, fee funding left over is swept to Savings, and Spark stops sending, receiving and optimizing leaves while the exit runs.
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