You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement FFOR offline Lightning receive in Synonym's rust-lightning/ldk-node stack, interoperable with the current Beignet implementation, and ship it through the generated Kotlin and Swift bindings to Bitkit Android and Bitkit iOS.
The end-to-end outcome is:
Bitkit prepares an eligible channel and a bounded set of receive vouchers while online.
Bitkit exposes an invoice only after the voucher commitments, activation transcript, and required recovery service acknowledgements are durable.
The user closes the app or the device loses connectivity. The wallet process can be completely stopped.
An ordinary compatible Lightning payer pays the invoice. The payer receives the preimage and reaches a terminal successful payment without waiting for Bitkit to reconnect.
Bitkit later reconnects, verifies the payment evidence, reconciles the vouchers, and records the correct payment and spendable balance without duplication.
If the settlement peer disappears or refuses to cooperate, Bitkit can recover the required preimage from an available witness or payer and enforce its signed claim on-chain before the claim deadline.
A library demo against Beignet is an intermediate milestone. This tracker is complete only after the supported production settlement peer and both Bitkit apps pass the acceptance gates below.
All implementation and verification checkboxes below are initially outstanding. Source references and existing test definitions are design inputs, not evidence that this port has already passed those tests.
Reconfirm these references before starting implementation and pin the actual interop fixtures and dependency revisions used by CI. Do not assume a developer checkout, a released package, and repository main have identical dependency patches.
Important baseline corrections:
Target Variant D and the D-R receipt-witness profile. The older feat/ffor branch primarily implements the A/B fast-forward and tower design.
Some README and spec status paragraphs still describe Variant D as unprototyped. Current Beignet source and tests implement it. Resolve normative disagreements explicitly against the pinned spec and errata; do not silently copy whichever implementation is easier.
Variant D uses ordinary BOLT 2 voucher HTLC commitment rounds. It does not need A/B unilateral commitment advancement, escape transactions, or commitment-number exceptions.
Use ffor-variant-d-vectors.md for the Variant D port. The older Appendix A vectors describe a different construction and are not a substitute.
The receiver role belongs in this stack. Blocktank currently uses LND, so compatible settlement support is a required external dependency, with its own linked implementation work.
Primary references:
Specification, especially sections 5, 7.5, 7.6, 9.5, 9.6, 9.7, 11, 13.7, 17, and Appendix F.
Variant D receiver support with fixed-amount, single-part payments over eligible ECDSA anchor channels.
A fixed-amount BOLT 11 invoice can be prepared and shared before Bitkit goes offline.
D-R witness provisioning, receipt retrieval, and recovery are part of the intended Bitkit deployment. Plain Variant D without witnesses may be used in the initial harness, with its weaker recovery assumption made explicit.
Cooperative reconciliation, force-close recovery, persistence, backups, Kotlin/Swift bindings, and both mobile integrations are required.
The feature is explicitly enabled and requires negotiated peer support. Existing online receive behavior remains supported.
Constraints that must appear in the design and product behavior
An epoch reserves a finite voucher book. Amounts, slot count, budget, and deadlines are fixed before the receiver goes offline. This does not provide unbounded or arbitrary-amount offline receive.
Cooperative credit preserves d_k in millisatoshis; on-chain outputs use the specified satoshi rounding and recovery incurs chain fees. Prefer whole-satoshi slots in the initial product and show recovery proceeds honestly.
Each voucher must survive dust trimming and satisfy the actual negotiated HTLC count, in-flight value, reserve, and commitment-fee constraints. 483 is a protocol upper bound, not a promised usable capacity.
The channel is frozen for normal updates while the epoch is active. Startup and send flows must reconcile it before treating it as an ordinary usable channel again.
The receiver must retrieve evidence and reconcile or enforce before T_exp with sufficient claim margin. No indefinite offline-recovery guarantee is made.
Watchtower-free operation requires a sufficiently long to_self_delay on the settlement peer's outputs, negotiated at channel open and accepted by that peer. Existing channels that do not meet the chosen policy cannot be silently upgraded. Either open a suitable channel or specify and validate the necessary chain-watch service.
Recovery needs both signed channel state and the required preimages. A seed alone is not a substitute for the epoch record or witness data.
Witness availability is an explicit assumption. Multiple services under one operator share an administrative failure domain.
A witness has a bounded store-before-propagate barrier. Its deadline path can propagate before storage succeeds and later mark a record unbarriered. Do not claim unconditional durable receipt availability from payer success.
Invoice/hash reuse is a documented protocol limitation. A malicious settlement peer or on-path witness that knows a preimage can settle another payment on the same hash without another receiver credit. Honest duplicate rejection and single-use invoice distribution mitigate this, but do not cryptographically close it. BOLT 12 issuance alone does not eliminate the limitation.
MPP/AMP, amountless receive, simple-taproot channels, A/B/C variants, automatic multi-peer budget allocation, and indefinite terminal recovery are outside the initial profile.
Reusable offers
A BOLT 12 issuer is a separately gated extension in milestone M10. It is needed if the product must serve new invoice requests while Bitkit is offline. It still draws from the precommitted fixed-amount book. Completing the initial BOLT 11 profile does not imply Lightning Address/LNURL support or unrestricted arbitrary-amount offers.
Protect the user's existing balance, every enforceable paid voucher, the settlement peer's unpaid liquidity, channel signing material, and the accuracy of application payment history.
Assume:
A peer can send malformed or contradictory signed messages, omit acknowledgements, replay old epochs, withhold preimages, disappear, or publish a revoked commitment.
A witness can fail, lose records, withhold responses, equivocate, or return malformed ciphertext and paging cursors.
The mobile process can terminate at any instruction, including during activation, receipt persistence, reconciliation, or event handling.
Storage can fail or contain an older backup. An app update, rollback, or second restored device can expose incompatible or stale state.
Payments, close requests, disconnects, and block notifications can race. Chain reorgs and high fees can consume recovery margins.
Inputs and peer capabilities are untrusted until validated. UI state, a push notification, or an HTTP success response is not evidence of an enforceable Lightning credit.
Ownership
Layer
Responsibilities
rust-lightning
Channel-owned epoch state, voucher recognition, commitment and signer integration, freeze enforcement, reestablishment, monitor persistence and on-chain claim safety
ldk-node
High-level receiver service, configuration, public operations, witness client orchestration, payment history/events, startup recovery, storage integration and bindings
Kotlin/Swift applications
User intent, readiness and recovery UI, invoice sharing, lifecycle coordination and display of verified results
Blocktank settlement implementation
Settlement role, admission policy, voucher setup, irreversible upstream settlement, durable close accounting and interoperability
Witness/issuer services
Receipt durability and retrieval; optional single-slot invoice issuance
Do not create a second independent channel state machine in ldk-node or the apps. Pure codecs, checked amount calculations, transcript hashing, and validated protocol types can be isolated for testing. Channel transitions must remain owned by the component that signs and persists the channel.
Custom peer messages are a transport mechanism, not sufficient authority to mutate commitments. Existing signing and monitor APIs should be extended narrowly where required. Keep keys inside the existing Rust signer boundary; do not export funding, revocation, or HTLC private keys to Kotlin/Swift or a service.
4. Protocol invariants
These are required review and test assertions, not optional implementation suggestions.
Activation and voucher commitments
Both parties negotiate the same profile, parameters, hash/amount pairs, channel, and epoch before signing vouchers.
Every voucher is irrevocably committed in both commitment views before either side sends stfu for activation.
All required second-stage signatures verify and every voucher remains an enforceable output in both relevant views.
Recompute T_init, T_setup, H_book, H_commit, and H_act from local validated state. Verify the peer's node-key signatures over the exact domain-separated wire representation.
Preserve canonical integer encoding, internal txid byte order, one-based slot indices, and LSB-first close bitmap encoding.
No ordinary update is emitted between either stfu and the activation acknowledgement.
S persists the full ACTIVE record before sending ff_activate_ack; R persists it before reporting readiness or exposing an invoice.
The durable ACTIVE freeze survives disconnect and restart, independently of BOLT 2 quiescence.
R does not know a voucher preimage at setup. Reject prohibited bindings to already-revealed commitment secrets, including checks repeated when the voucher round reveals another secret.
A prepared voucher is not an incoming paid invoice and does not create a payment-received notification.
Settlement and reconciliation
S releases a preimage only against the matching irrevocably committed upstream HTLC and its durable slot transition.
The invoice and voucher amount are d_k. The last-hop forwarding fee is added upstream and is not subtracted from the voucher or charged twice.
SETTLING is durable before the fulfil leaves. Recovery identifies the specific upstream circuit, not just a hash shared by multiple attempts.
A slot in SETTLING counts as settled in ff_close_ack and includes its preimage. Restart must finish the owed fulfil even when admission has since closed.
Close and settlement races have a durable serialization point. A crash cannot move a payment to the other side of the close bitmap.
R verifies all ack fields and preimages. It never fails a voucher for which it holds a valid preimage, even if the peer's bitmap clears that bit.
R never fails a slot declared settled merely because its preimage is missing. Reject the malformed acknowledgement and preserve the claim/recovery path.
CLOSED means all vouchers are irrevocably resolved in both views. It does not mean a close request was sent or the local HTLC map became empty.
Variant D uses unchanged BOLT 2 commitment-number rules. Do not port the A/B reestablishment exceptions.
Replay and recovery
Identical replay is idempotent. Conflicting authenticated replay is a protocol error with retained evidence.
Epoch identifiers cannot be reused after refusal, abort, closure, or restart.
Setup abort normally unwinds any real committed voucher HTLCs. It never deletes those liabilities from memory or storage.
Preserve the acknowledgement-loss exception: an ACTIVATING receiver retains enough state to accept a matching retransmitted activation ack from a peer already durably ACTIVE.
An active epoch cannot be aborted. Normal exit requires drain or on-chain enforcement.
Recovery, monitor updates, event replay, and backup restore never turn stale state into permission to issue another invoice or spend a reserved claim.
5. Implementation map
Paths below identify current integration points. Proposed new names are illustrative and require API review.
Consistent Rust/Kotlin/Swift types, errors and examples
Tests/CI
tests/, existing LND/VSS integration harnesses and workflows
Beignet interop, real LND settlement, fault injection and device acceptance
The present NodeCustomMessageHandler supports ignoring messages or handling liquidity messages. FFOR must coexist with that functionality. A single new handler that accidentally disables LSPS is unacceptable.
6. Milestone M0: freeze the supported profile and dependency plan
Confirm the exact rust-lightning base and whether to extend Synonym's fork or prepare upstreamable changes. Pin immutable revisions for every affected Lightning crate without accidentally changing the legacy migration-test dependency graph.
Confirm Blocktank's deployed LND version/fork, channel types, maximum accepted delays, HTLC limits, and operational upgrade constraints.
Record the initially supported profile: Variant D, anchors, fixed amounts, single-part, one active epoch per channel, BOLT 11 first, D-R recovery required for the intended app rollout.
Choose maximum budget, slot count, offline admission window, claim margin, witness policy, and on-chain fee reserve policy. Establish which values are protocol bounds versus deployment limits.
Record channel eligibility and migration behavior for existing users, including users with no channel or inadequate inbound liquidity. Complete channel provisioning before voucher setup; do not assume a new channel can be signed after the phone is offline.
Compare the spec, errata, and Beignet behavior and record every unresolved discrepancy before relying on it.
Write an architecture decision describing the state owner, durability protocol, recovery ordering, and downgrade policy.
Add linked child issues for the core port, node/bindings, Blocktank settlement, witnesses, Android, iOS, and verification. This tracker owns the cross-repository completion gate.
Exit gate: reviewed profile, immutable reference inputs, explicit product constraints, and named owners for the external services. Unknown deployment parameters remain visible blockers rather than guessed defaults.
7. Milestone M1: codecs, validated types, arithmetic and vectors
Implement only the agreed profile while rejecting unsupported variants explicitly. Feature/message identifiers in the draft are provisional, including option_ff_receive bits 560/561. Centralize them and document compatibility policy.
Implement ff_init, ff_accept, ff_activate, ff_activate_ack, ff_abort, ff_close, ff_close_ack, applicable error handling, and the reestablishment TLV.
Support the required fixed-amount and transcript TLVs. Treat witness peers and hash-chain options according to the selected profile rather than silently ignoring behavior-changing input.
Use bounded decoding, canonical TLVs, checked length/count arithmetic, strict field validation, unknown mandatory-TLV rejection, and total message limits compatible with BOLT 8.
Implement checked fee arithmetic from section 7.6. Cover underpayment, excess payee amount, allowed upstream fee overpayment, and blinded-path rounding separately.
Validate dust, both commitment views, funder-specific reserve obligations, maximum in-flight value, fee buffers, anchors, and slot count. Include the S-funded, zero-R-balance vector.
Do not copy fixture channel-delay values into production policy. The canonical vectors test transaction construction; their sample delays do not establish a safe offline window.
Validate deadline ordering and unknown/stale chain-tip behavior. Treat wall-clock invoice expiry as a conservative estimate; authoritative admission and claims use block heights.
Reproduce the Variant D transaction bytes, txids, voucher scripts, transcript hashes, and bitmap layout. Verify signatures with the target implementation; signature byte equality is required only where the fixture fixes the signing inputs and nonce behavior.
Reuse established secp256k1, hashing, HKDF, and AEAD implementations. Review signer domain separation, secret lifetime, randomness and nonce/key uniqueness at the new boundaries.
Add unit/property tests and fuzz targets for decoders, signed transcript parsing, books, amount bounds, and invalid combinations. No panic or unbounded allocation on hostile input.
Exit gate: Rust and Beignet agree on the committed state and canonical vectors before a real channel is activated.
The ACTIVATING reconnect exception and on-chain exits must be represented explicitly. On-chain enforcement is not an invented wire state.
Match setup HTLCs by channel, epoch, negotiated HTLC id, hash, amount and expiry. Validate the specified voucher onion without processing it as an ordinary incoming payment.
Park matching vouchers without auto-claim, ordinary invoice failure, MPP grouping, or forwarding. A malformed/mismatching setup must unwind safely.
Integrate with the ordinary commitment/signature/revocation flow until both views contain every voucher and all required signatures are present.
Expose a production-safe activation/quiescence integration. Do not depend on test-only methods or treat an idle peer connection as quiescence.
Persist lifecycle transitions through the existing channel/monitor ordering rules. A failed or pending monitor persistence operation blocks dependent signatures, acknowledgements and readiness.
Enforce the active freeze across adds, fulfils/fails, fee updates, new commitment rounds, stfu, splice, and cooperative close negotiation, while permitting protocol-required replays.
Implement drain-only permissions after a valid close ack and prevent normal traffic from overtaking recovery after reconnect.
Treat activation-hash disagreement as a recovery/enforcement condition. Do not silently reset the epoch and resume normal activity.
Scope timer behavior to state. The setup timeout must not abort a durably active epoch or destroy an unresolved acknowledgement-loss record.
Review lock ordering, callback reentrancy, Send/Sync, cancellation and drop behavior. Avoid network I/O under channel locks and new unjustified unsafe code.
Exit gate: a Beignet settlement peer and the Rust receiver complete activation, disconnect, independently restart, and converge through a correct drain over real peer transport.
9. Milestone M3: persistence, restoration and on-chain enforcement
Persist enough information to recover without the settlement peer, including validated terms/book, channel/epoch identity, transcript and activation evidence, current lifecycle, invoice exposure state, verified preimages, witness metadata and key references, close evidence, and replay/event progress. Preserve signed claim material through the existing channel monitors and signer architecture rather than duplicating a second mutable transaction database.
Specify how epoch records, ChannelManager, ChannelMonitor updates, and application history recover consistently when writes are separate. Do not assume cross-key or cross-store atomicity that an adapter does not provide.
Add versioned serialization and schema migration with forward-compatibility and downgrade rules for unresolved epochs.
Prove recovery with each storage backend supported for the feature, including the VSS configuration actually used by Bitkit. Explicitly reject unsupported configurations.
A storage failure must not report ACTIVE, expose an invoice, discard a preimage, or acknowledge a safety-critical transition that was not durable.
Store verified preimages in the monitor path before reporting a recoverable credit. Cover retrieval after a funding spend has already been observed.
Recover paid vouchers when R publishes its commitment and when S publishes its current commitment. Exercise the actual signer, fee bumping, sweep destination, and claim scheduler.
Let S reclaim unpaid vouchers only under the proper HTLC timeout conditions. Preserve R's unrelated balance.
Prove punishment of a revoked pre-epoch S commitment after the entire supported offline window, using the negotiated delay and real claim margins.
Cover delayed chain synchronization, fee spikes, insufficient fee reserves, chain reorgs and disconnected chain sources. Surface actionable recovery status without claiming that a broadcast is confirmed.
Preserve invoice exposure and epoch uniqueness across restored backups. A restore must not reissue a consumed slot or release an uncertain claim.
Define the supported policy for stale backups, missing witness keys, concurrent restored devices, and legacy recovery paths. Never bypass monitor consistency to make the feature appear usable.
Provision a sweep destination and adequate fee funding before reporting offline readiness. An anchor output alone is not a fee budget.
Exit gate: killed processes and failed writes at each critical boundary recover the same liabilities and evidence. Real regtest transactions demonstrate both receiver claim paths, peer timeout, and stale-state justice.
10. Milestone M4: ldk-node API, payment history and events
Expose a focused receiver API. The following names describe required capabilities and are not final Rust or UDL signatures:
Proposed operation
Required behavior
offline_receive().capabilities(peer, channel)
Eligibility, negotiated support, configured profile, limits and precise refusal reasons
prepare_epoch(channel, amounts, policy)
Start/resume a correlated preparation operation; return an operation/epoch reference, not premature readiness
epoch_status(epoch) / list_epochs()
Durable lifecycle, remaining unexposed slots, budget, deadline heights, recovery health and current restrictions
create_invoice(epoch, slot, description)
Expose exactly the authorized fixed amount/hash once ready, with signed terms and bounded expiry
close_epoch(epoch)
Request admission stop and drain; completion is observed separately and is idempotent
recover_epoch(epoch)
Fetch/verify receipts, reconcile if possible, and report any remaining evidence or chain action needed
import_payment_proof(epoch, proof)
Accept a payer-supplied preimage/proof through full hash, slot and epoch validation
enforce_epoch(epoch)
Use the existing force-close/claim machinery with an explicit, reviewable result and cost semantics
Wire configuration end to end from Builder through runtime, feature negotiation and status. Disabled/unsupported modes must not advertise readiness.
Keep the Rust implementation authoritative. The app never implements fee arithmetic, commitment verification, transcript validation, or preimage storage policy itself.
Make cancellation semantics explicit. A caller timeout or dropped future does not prove that an activation/close failed or that it is safe to start a new epoch.
Make invoice creation retry-safe. Persist the selected slot and canonical invoice/exposure result so a crash around the response cannot issue it as a different logical invoice. Returning the same authorized invoice to the same operation is distinct from allocating the slot again.
Keep slot selection and exposure atomic within an epoch. Concurrent UI requests cannot select the same available slot independently.
Suppress ordinary auto-claim behavior for setup vouchers in src/event.rs.
Specify event states for readiness, verified paid voucher, reconciliation progress, deadline/recovery warning, on-chain claim progress and closure.
Separate reserved budget, evidenced but unresolved credit, spendable Lightning balance and on-chain pending claims. Never count all prepared vouchers as received money.
Correlate events with channel, epoch, slot and payment identity. Preserve receipt provenance and distinguish peer assertions from verified preimages.
Do not infer payment failure from missing local history, an expired API timeout, or the absence of a witness response.
Keep notification/event acknowledgement retry-safe and document when event_handled() may be called.
Exit gate: public Rust API tests cover the complete receiver lifecycle without reaching into private channel state, and an interrupted caller can recover the operation's actual outcome.
11. Milestone M5: D-R witness client and service contract
Port the receiver side of section 9.6 and Appendix F. A compatible Beignet witness can be the initial reference service. A production service and its operator/failure assumptions must be identified before app rollout.
Generate and securely persist per-epoch fetch/encryption keys, mailbox identity, expected witness identity, manifest, H_act, retention deadline and provisioning state.
Provision via authenticated transport and signed manifests. Verify the witness acknowledgement and retention promise before depending on it.
Require all witnesses selected for readiness to acknowledge before exposing invoices. Failed provisioning has an explicit retry/removal policy, with no silent weakening of the chosen profile.
Configure invoice paths and negotiated witness peers so conforming payments traverse the intended witness. Verify this in the wire-level harness, not just in UI configuration.
Fetch with signed requests and fresh nonces. Implement v0.9.3 authenticated paging, increasing cursors, record bounds, maximum page count, and termination on malicious/non-progressing responses.
Verify record signature, expected witness, version/profile, mailbox, activation hash, terms hash, slot, epoch, amounts, deadlines, ciphertext hash and decrypted preimage before crediting anything.
Implement Appendix F.3 exactly: secp256k1 ECDH, specified hashing and HKDF, ChaCha20-Poly1305, and AAD with ciphertext_hash zeroed. Match Beignet's ECDH hashing convention to avoid a double-hash mismatch. A fixed AEAD nonce is valid only with a fresh per-record derived key; add negative and cross-language vectors.
Preserve unbarriered and service-health information. A valid late record can still recover a voucher; its flag must not be confused with proof that storage preceded payer success.
Deduplicate the same preimage from S, a payer, or multiple witnesses. Handle one witness withholding while another returns valid evidence.
Treat guardian receipts only as storage acknowledgements under their documented assumptions. Refuse unsupported min_receipts policies rather than claiming them satisfied.
Close witness/issuer service activity explicitly while preserving records for the agreed recovery/audit retention period. Do not garbage-collect unresolved claims or required evidence.
Protect fetch keys and decrypted preimages from logs, analytics, exception strings and ordinary UI export.
Required service-side contract, whether implemented in Beignet, LND or another reviewed service:
The witness records the matching preimage durably before propagating fulfilment during the normal path.
The barrier is bounded by wall-clock and CLTV safety, with a safety delta larger than the witness's own force-close/claim buffer.
Deadline fallback, late storage, retries, restart and idempotency match the spec and expose unbarriered accurately.
Provisioned manifests and records rehydrate after service restart while R remains offline.
Capacity, retention, authentication, rate limits and privacy protections apply to real network requests.
Exit gate: stop S permanently after payer success. R restarts, retrieves evidence from W alone, verifies it, and confirms the voucher claim on regtest with no assistance from S or the payer.
This is a required production dependency. An unmodified LND settlement peer does not implement FFOR.
Create and link the Blocktank/LND companion issue against the actual deployed revision and assign ownership.
Identify the minimum engine changes versus service orchestration. Existing custom-message RPCs and HtlcInterceptor are useful, but do not alone establish channel-owned activation, durable freeze or recovery semantics.
Implement Variant D negotiation, S-generated preimages, voucher HTLC setup, both-view commitment verification, activation and signed close acknowledgements.
Integrate the durable UNUSED -> SETTLING -> SETTLED slot ledger with upstream HTLC/circuit identity and LND's persistence/contract resolution.
Ensure settlement occurs before the normal offline-next-hop failure path for eligible delegated payments, without attempting a fresh HTLC to the disconnected recipient.
Enforce the fixed amount/fee/deadline policy, honest duplicate rejection, configured budgets, eligible peers and required witness routing policy.
Serialize incoming settlement with close and block-height admission changes. Recover interrupted fulfils without losing upstream claims or changing the signed bitmap.
Protect the active channel from ordinary updates and apply correct drain/reestablishment behavior after independent LND and mobile restarts.
Prove current-commitment recovery, peer timeout, force-close and breach handling across the LND/LDK boundary.
If LND also serves as a witness, add the actual fulfil-path durability barrier. An asynchronous HTLC event subscription is not a substitute.
Keep administrative APIs authenticated and scoped, and expose configuration/status that proves the runtime role is enabled.
Define rolling upgrade, schema compatibility, draining and rollback procedures for nodes holding active epochs. Turning off new admissions must not disable recovery of existing epochs.
Exit gate: an unmodified external payer completes a single-part payment through the supported Blocktank deployment while the actual LDK receiver process is stopped. Both parties later recover correct balances, including independently injected restarts.
13. Milestone M7: Kotlin and Swift bindings
Add the reviewed API, records, enums, errors and events to bindings/ldk_node.udl and the required Rust FFI conversions.
Use explicit integer units and bounded collections. Preserve full millisatoshi and block-height values through both languages.
Expose statuses and evidence summaries, not private keys, mutable protocol internals or application-controlled signature operations.
Verify object ownership, callback threading, reentrancy, lifecycle cancellation and shutdown while an operation is in progress.
Add compiling Rust, Kotlin and Swift examples for prepare, ready invoice, resume/reconcile, witness recovery and force-close status.
Regenerate all bindings using ./bindgen.sh, run in the background as required by this repository. Do not run the individual generation scripts or hand-edit generated Kotlin/Swift code.
Compile the generated Android and Swift artifacts and exercise the public API through each binding.
Coordinate enum/API changes and persistence compatibility so an app rollback cannot misread an unresolved epoch.
Exit gate: the released artifacts expose the same validated receiver behavior on Android and iOS, including errors and replayed events.
14. Milestone M8: Bitkit Android and iOS integration
Implement and verify each requirement on both platforms:
Update the dependency to the tested FFOR-capable binding release.
Add an explicit receive flow for the supported fixed-amount profile. Retain ordinary receive when the peer/channel is ineligible, with an honest explanation of which behavior applies.
Start preparation while the app is online and present an invoice only after the library reports durable readiness. Do not rely on a last background callback to prepare an epoch.
Show the supported amount, admission expiry, remaining capacity, readiness/recovery state and any required user action using library-provided data.
Disable or explain unsupported amountless/MPP behavior. Do not relabel an ordinary online-only invoice as offline-capable.
Resume and reconcile active epochs before using the frozen channel for sends or new channel mutations. Support other independently usable channels according to library state.
Fetch witness records and import verified outcomes on startup, foreground, and network recovery without duplicating history or notifications.
Treat push notifications as hints. Payment success and balance updates depend on verified library state, and payment settlement must work without push delivery.
Handle process death, force-stop, device reboot, airplane mode, OS suspension, loss of background execution and a long offline interval inside the supported deadline.
Preserve epoch metadata and key references through the application's actual backup/restore flow. Handle stale restore and another restored device under the agreed policy.
Show an unresolved recovery state when evidence is unavailable. Do not turn a temporarily missing receipt into payment failure or silently release reserved value.
Present on-chain recovery progress and fees accurately. A pending claim is not spendable Lightning balance.
On feature disable or app downgrade, preserve active obligations and recovery. A remote kill switch may stop new epochs; it must not delete existing ones.
Exit gate: physical-device tests on Android and iOS demonstrate payer success with the wallet process absent, followed by correct reconciliation, history, balances and notifications. Repeat with S unavailable and witness-based recovery.
15. Milestone M9: verification and release qualification
Cross-implementation matrix
Payer
Settlement peer
Receiver
Witness
Purpose
Beignet
Beignet
Rust receiver harness
None initially
Differential protocol development
Stock LND
Beignet
ldk-node
Beignet
Ordinary BOLT 11 payer compatibility
Another supported stock payer
Beignet
ldk-node
Beignet
Independent payer behavior and single-part limits
Stock payer
Modified Blocktank LND
ldk-node
Compatible witness
Required production engine path
Stock payer
Modified Blocktank LND
Bitkit Android
Production-equivalent witness
Android product gate
Stock payer
Modified Blocktank LND
Bitkit iOS
Production-equivalent witness
iOS product gate
Use separate processes and real BOLT 8/TCP connections. In-memory loopback tests supplement this matrix but do not replace it. Stop the receiver process and isolate its network during the offline payment. Assert the payer's terminal success and preimage before restarting it.
Mandatory fault and boundary matrix
Case
Required observation
Before/after each setup commitment and revocation
Both sides recover signed state; partial setup unwinds without a false payment
S persisted ACTIVE, activation ack lost
R retains ACTIVATING, receives matching replay, and becomes ACTIVE exactly once
R fails to persist ACTIVE
No ready event or invoice exposure
Disconnect/restart in ACTIVE
Freeze remains; no accidental ordinary update
Invoice exposure write/response crash
Same correlated result is recoverable; slot is not independently reissued
Concurrent same-slot invoice requests
One allocation/exposure outcome
S crash before/after SETTLING and fulfil
No unbacked reveal; owed fulfil resumes with the same circuit identity
Payment races close/deadline
Durable admission order and a consistent final bitmap
Close ack lost; one side DRAINING and other ACTIVE
Correct replay precedes drain traffic
Both sides restart during drain
Idempotent fulfil/fail; CLOSED only after both commitments resolve
S lies in bitmap
Valid known preimages remain claimable; contradictory evidence retained
W fails before/after record persistence and propagation
Barrier ordering, deadline fallback and replay behave as specified
W pages backwards, repeats, truncates or exceeds bounds
Client terminates safely and retains already verified records
One witness withholds; another responds
Recovery credits the voucher once
All evidence sources unavailable
Explicit unresolved recovery and deadline behavior; no invented successful claim
Force-close by R and by S
Correct claims from both views, with real mined sweeps
No activation of an invalid or unenforceable voucher book
Ordinary LSPS, online receives, sends and non-FFOR peers
Existing behavior remains covered
Same invoice paid twice, concurrent reuse, ambiguous retry, MPP attempt
Honest refusal plus explicit characterization of the malicious same-hash limitation
For hash reuse, preserve the distinction between a test that proves an honest peer rejects duplicates and a characterization test that demonstrates a malicious preimage holder can still take a reused-hash payment. The latter must not be presented as a fixed security property merely because the test passes.
Also preserve public-surface coverage from tests/cli/ffor-surface.test.ts and tests/cli/daemon-options.test.ts where applicable.
Rust and integration qualification
Unit tests for all new business logic, including state transitions and checked arithmetic.
Property/model tests over valid and adversarial transition sequences, replay permutations, budgets and slot accounting.
Fuzz targets for wire messages, books, restoration records and witness decryption/validation boundaries.
Concurrency/cancellation tests for activation, close, event handling, storage callbacks and node shutdown; use targeted model checking where it meaningfully covers shared-state ordering.
Measured line and branch coverage for new protocol logic, targeting complete coverage of safety-critical transitions and rejection branches. Publish actual reports and explain any unreachable/uncovered cases; do not substitute estimated coverage for executed evidence.
Persist-to-wire fault injection at every safety boundary, with actual process kills in addition to simulated errors.
On-chain regtest enforcement tests that mine commitment, HTLC and final sweep/justice transactions rather than only checking signatures or mempool acceptance.
Required CI fails when an expected daemon, fixture, feature or infrastructure dependency is absent. Missing prerequisites cannot produce a silently green skipped gate.
Independent protocol and implementation review, with findings resolved or explicit release-blocking limitations recorded.
Run and attach results for the actual implementation revision, using the repository's existing infrastructure setup and required environment variables:
cargo fmt --check
cargo build --all-targets --locked
cargo test --lib
cargo test --all-targets
cargo test --features uniffi
cargo test --doc
cargo clippy --all-targets
RUSTFLAGS="--cfg lnd_test" cargo test --test integration_tests_lnd
RUSTFLAGS="--cfg vss_test" cargo test --test integration_tests_vss
Add the new named FFOR test targets, fuzz commands and platform jobs when implemented. On macOS raise the file-descriptor limit as documented before infrastructure tests. Run Miri on compatible targeted Rust tests where it can validate unsafe/ownership concerns, and record unsupported cases accurately. Review warnings rather than declaring a clean build without captured output.
Exit gate: executed cross-engine, storage, on-chain and device evidence at pinned revisions, including the negative cases. A green Beignet suite alone does not qualify this port.
This milestone is required if the release promises that unknown payers can request fresh invoices while Bitkit remains offline. It may ship after the fixed-invoice release, but must not be implied by it.
Provision the issuer only after activation and its witness provisioning; verify ownership of the offer path and the receiver's signed attestation.
Build offer paths that the issuer can actually validate, following section 9.7's introduction/terminal-node requirements.
Serve each request using one unconsumed fixed-amount slot, with durable single issuance before returning the invoice.
Build fresh blinded payment paths through the required witness and settlement peer, bound to the negotiated relay terms and deadlines.
Preserve privacy through fixed refusals and avoid disclosing the voucher book to probing requesters.
Retire offers on deadline/close while retaining the required fixed refusal and recovery records.
Prove an unmodified supported BOLT 12 payer can obtain and pay the invoice with R offline, including witness-only recovery afterwards.
Preserve and document the same-hash reuse limitation. Do not describe delegated issuance as a cryptographic fix for it.
Design and test any separate Lightning Address/LNURL adapter explicitly before advertising that capability.
17. Rollout, documentation and completion criteria
Rollout
Start with regtest, then controlled funded testing, then a limited opt-in release with explicit budgets and supported windows.
Deploy and verify settlement/witness capability before mobile clients expose offline-ready invoices.
Monitor activation failures, pending age, deadline proximity, receipt-fetch failures, unbarriered records, recoveries, reserved liquidity and force-close outcomes without logging secrets or sensitive payment payloads.
Document capacity planning for booked liquidity, HTLC slots, witness storage, fee reserves and mass recovery during a service outage.
Define upgrade and rollback handling for unresolved epochs, including the exact point after which an old binary cannot safely open the store.
A disable switch stops new preparation/admission where permitted while continuing evidence retention, drain, enforcement and user recovery.
Follow the repository's release procedure: all required version files, cumulative fork changelog, generated bindings, committed and pushed release contents before tagging, matching Swift archive checksum, and consumer-facing delta release notes.
Documentation
Publish the supported profile, threat model, availability assumptions, hash-reuse limitation, finite capacity, deadlines and channel eligibility requirements.
Document each public API's purpose, preconditions, idempotency, cancellation behavior, persistence guarantees and examples, with compiling doctests where applicable.
Document backup/restore requirements, witness key retention, on-chain fee funding and the user-facing recovery procedure.
Add an operator runbook for Blocktank and witnesses, including service outage and rolling upgrade scenarios.
Add reproducible local/CI setup, exact interop revisions, fault-injection instructions and a fuzzing guide.
Definition of done
rust-lightning implements the reviewed Variant D receiver profile with no duplicate channel authority and no unjustified unsafe code.
Kotlin and Swift artifacts compile and behave consistently through the generated bindings.
The supported Blocktank settlement peer interoperates with the released mobile library.
Android and iOS both receive a payment with their wallet process stopped; the payer finishes before either app resumes.
Both apps later reconcile correct spendable balances, history and notifications without duplicate credit.
With the settlement peer unavailable, witness/payer evidence plus persisted channel state produces confirmed on-chain recovery within the supported bounds.
Crash, replay, storage failure, restore, deadline and reorg tests pass with captured evidence.
Ordinary Lightning and LSPS behavior remains covered and operational.
Security limitations and release policy are reviewed explicitly; no unsupported claim of indefinite recovery, arbitrary amounts or cryptographic invoice-reuse prevention is made.
Documentation, measured coverage, required builds/tests, independent review and release artifacts are complete.
Closing this tracker requires links to the implementation PRs, exact tested artifact versions, CI runs, interoperability results and Android/iOS acceptance evidence. Creating the APIs or demonstrating only the happy path is insufficient.
Goal
Implement FFOR offline Lightning receive in Synonym's rust-lightning/ldk-node stack, interoperable with the current Beignet implementation, and ship it through the generated Kotlin and Swift bindings to Bitkit Android and Bitkit iOS.
The end-to-end outcome is:
A library demo against Beignet is an intermediate milestone. This tracker is complete only after the supported production settlement peer and both Bitkit apps pass the acceptance gates below.
All implementation and verification checkboxes below are initially outstanding. Source references and existing test definitions are design inputs, not evidence that this port has already passed those tests.
1. Baseline and source of truth
Snapshot inspected on 2026-09-17:
80cafcd79555835e227d968b62ff3f6f74cbbc309ea018b6371c8b22366a133bc504679ac04b830e26664614696e82dc10e10f5c2042cc9ac1bb8f5e0.7.0-rc.66; direct Lightning dependency resolves to registrylightning 0.2.5in this snapshot's lockfile521332d04e082e5ae2f22d9ec35621cfcf89ca69ldk-node-android 0.7.0-rc.66a84f00395bd05f6918ede4f6d169a462a8e4195b0.7.0-rc.66packageReconfirm these references before starting implementation and pin the actual interop fixtures and dependency revisions used by CI. Do not assume a developer checkout, a released package, and repository main have identical dependency patches.
Important baseline corrections:
feat/fforbranch primarily implements the A/B fast-forward and tower design.ffor-variant-d-vectors.mdfor the Variant D port. The older Appendix A vectors describe a different construction and are not a substitute.Primary references:
2. Product scope and honest guarantees
First supported release
Constraints that must appear in the design and product behavior
d_kin millisatoshis; on-chain outputs use the specified satoshi rounding and recovery incurs chain fees. Prefer whole-satoshi slots in the initial product and show recovery proceeds honestly.483is a protocol upper bound, not a promised usable capacity.T_expwith sufficient claim margin. No indefinite offline-recovery guarantee is made.to_self_delayon the settlement peer's outputs, negotiated at channel open and accepted by that peer. Existing channels that do not meet the chosen policy cannot be silently upgraded. Either open a suitable channel or specify and validate the necessary chain-watch service.unbarriered. Do not claim unconditional durable receipt availability from payer success.Reusable offers
A BOLT 12 issuer is a separately gated extension in milestone M10. It is needed if the product must serve new invoice requests while Bitkit is offline. It still draws from the precommitted fixed-amount book. Completing the initial BOLT 11 profile does not imply Lightning Address/LNURL support or unrestricted arbitrary-amount offers.
Deferred terminal recovery is tracked upstream in coreyphillips/ffor#32.
3. Threat model and architectural boundaries
Protect the user's existing balance, every enforceable paid voucher, the settlement peer's unpaid liquidity, channel signing material, and the accuracy of application payment history.
Assume:
Ownership
Do not create a second independent channel state machine in ldk-node or the apps. Pure codecs, checked amount calculations, transcript hashing, and validated protocol types can be isolated for testing. Channel transitions must remain owned by the component that signs and persists the channel.
Custom peer messages are a transport mechanism, not sufficient authority to mutate commitments. Existing signing and monitor APIs should be extended narrowly where required. Keep keys inside the existing Rust signer boundary; do not export funding, revocation, or HTLC private keys to Kotlin/Swift or a service.
4. Protocol invariants
These are required review and test assertions, not optional implementation suggestions.
Activation and voucher commitments
stfufor activation.T_init,T_setup,H_book,H_commit, andH_actfrom local validated state. Verify the peer's node-key signatures over the exact domain-separated wire representation.stfuand the activation acknowledgement.Spersists the fullACTIVErecord before sendingff_activate_ack;Rpersists it before reporting readiness or exposing an invoice.ACTIVEfreeze survives disconnect and restart, independently of BOLT 2 quiescence.Rdoes not know a voucher preimage at setup. Reject prohibited bindings to already-revealed commitment secrets, including checks repeated when the voucher round reveals another secret.Settlement and reconciliation
Sreleases a preimage only against the matching irrevocably committed upstream HTLC and its durable slot transition.ACTIVE, deadlineD, upstream CLTV safety, fixed payee amount, fees, slot availability, and required witness path policy.d_k. The last-hop forwarding fee is added upstream and is not subtracted from the voucher or charged twice.SETTLINGis durable before the fulfil leaves. Recovery identifies the specific upstream circuit, not just a hash shared by multiple attempts.SETTLINGcounts as settled inff_close_ackand includes its preimage. Restart must finish the owed fulfil even when admission has since closed.Rverifies all ack fields and preimages. It never fails a voucher for which it holds a valid preimage, even if the peer's bitmap clears that bit.Rnever fails a slot declared settled merely because its preimage is missing. Reject the malformed acknowledgement and preserve the claim/recovery path.CLOSEDmeans all vouchers are irrevocably resolved in both views. It does not mean a close request was sent or the local HTLC map became empty.Replay and recovery
ACTIVATINGreceiver retains enough state to accept a matching retransmitted activation ack from a peer already durablyACTIVE.5. Implementation map
Paths below identify current integration points. Proposed new names are illustrative and require API review.
lightning/src/ln/channel.rs,channelmanager.rschannel_reestablishextensionlightning/src/chain/channelmonitor.rs, signer and on-chain transaction handlingsrc/config.rs,src/builder.rs,src/lib.rs,src/types.rssrc/message_handler.rssrc/payment/offline_receive.rsor focusedsrc/ffor/modulesrc/payment/bolt11.rs; laterbolt12.rssrc/event.rs,src/payment/store.rs,src/data_store.rssrc/io/, SQLite, filesystem and VSS adaptersbindings/ldk_node.udl,src/ffi/mod.rs,src/ffi/types.rs, generated bindingstests/, existing LND/VSS integration harnesses and workflowsThe present
NodeCustomMessageHandlersupports ignoring messages or handling liquidity messages. FFOR must coexist with that functionality. A single new handler that accidentally disables LSPS is unacceptable.6. Milestone M0: freeze the supported profile and dependency plan
Exit gate: reviewed profile, immutable reference inputs, explicit product constraints, and named owners for the external services. Unknown deployment parameters remain visible blockers rather than guessed defaults.
7. Milestone M1: codecs, validated types, arithmetic and vectors
option_ff_receivebits 560/561. Centralize them and document compatibility policy.ff_init,ff_accept,ff_activate,ff_activate_ack,ff_abort,ff_close,ff_close_ack, applicable error handling, and the reestablishment TLV.Exit gate: Rust and Beignet agree on the committed state and canonical vectors before a real channel is activated.
8. Milestone M2: rust-lightning receiver lifecycle
Implement the persistent lifecycle:
The
ACTIVATINGreconnect exception and on-chain exits must be represented explicitly. On-chain enforcement is not an invented wire state.stfu, splice, and cooperative close negotiation, while permitting protocol-required replays.Send/Sync, cancellation and drop behavior. Avoid network I/O under channel locks and new unjustifiedunsafecode.Exit gate: a Beignet settlement peer and the Rust receiver complete activation, disconnect, independently restart, and converge through a correct drain over real peer transport.
9. Milestone M3: persistence, restoration and on-chain enforcement
Persist enough information to recover without the settlement peer, including validated terms/book, channel/epoch identity, transcript and activation evidence, current lifecycle, invoice exposure state, verified preimages, witness metadata and key references, close evidence, and replay/event progress. Preserve signed claim material through the existing channel monitors and signer architecture rather than duplicating a second mutable transaction database.
ACTIVE, expose an invoice, discard a preimage, or acknowledge a safety-critical transition that was not durable.Exit gate: killed processes and failed writes at each critical boundary recover the same liabilities and evidence. Real regtest transactions demonstrate both receiver claim paths, peer timeout, and stale-state justice.
10. Milestone M4: ldk-node API, payment history and events
Expose a focused receiver API. The following names describe required capabilities and are not final Rust or UDL signatures:
offline_receive().capabilities(peer, channel)prepare_epoch(channel, amounts, policy)epoch_status(epoch)/list_epochs()create_invoice(epoch, slot, description)close_epoch(epoch)recover_epoch(epoch)import_payment_proof(epoch, proof)enforce_epoch(epoch)src/event.rs.event_handled()may be called.Exit gate: public Rust API tests cover the complete receiver lifecycle without reaching into private channel state, and an interrupted caller can recover the operation's actual outcome.
11. Milestone M5: D-R witness client and service contract
Port the receiver side of section 9.6 and Appendix F. A compatible Beignet witness can be the initial reference service. A production service and its operator/failure assumptions must be identified before app rollout.
H_act, retention deadline and provisioning state.ciphertext_hashzeroed. Match Beignet's ECDH hashing convention to avoid a double-hash mismatch. A fixed AEAD nonce is valid only with a fresh per-record derived key; add negative and cross-language vectors.unbarrieredand service-health information. A valid late record can still recover a voucher; its flag must not be confused with proof that storage preceded payer success.min_receiptspolicies rather than claiming them satisfied.Required service-side contract, whether implemented in Beignet, LND or another reviewed service:
unbarrieredaccurately.Exit gate: stop S permanently after payer success. R restarts, retrieves evidence from W alone, verifies it, and confirms the voucher claim on regtest with no assistance from S or the payer.
12. Milestone M6: Blocktank/LND settlement interoperability
This is a required production dependency. An unmodified LND settlement peer does not implement FFOR.
HtlcInterceptorare useful, but do not alone establish channel-owned activation, durable freeze or recovery semantics.UNUSED -> SETTLING -> SETTLEDslot ledger with upstream HTLC/circuit identity and LND's persistence/contract resolution.Exit gate: an unmodified external payer completes a single-part payment through the supported Blocktank deployment while the actual LDK receiver process is stopped. Both parties later recover correct balances, including independently injected restarts.
13. Milestone M7: Kotlin and Swift bindings
bindings/ldk_node.udland the required Rust FFI conversions../bindgen.sh, run in the background as required by this repository. Do not run the individual generation scripts or hand-edit generated Kotlin/Swift code.Exit gate: the released artifacts expose the same validated receiver behavior on Android and iOS, including errors and replayed events.
14. Milestone M8: Bitkit Android and iOS integration
Integration starting points:
Implement and verify each requirement on both platforms:
Exit gate: physical-device tests on Android and iOS demonstrate payer success with the wallet process absent, followed by correct reconciliation, history, balances and notifications. Repeat with S unavailable and witness-based recovery.
15. Milestone M9: verification and release qualification
Cross-implementation matrix
Use separate processes and real BOLT 8/TCP connections. In-memory loopback tests supplement this matrix but do not replace it. Stop the receiver process and isolate its network during the offline payment. Assert the payer's terminal success and preimage before restarting it.
Mandatory fault and boundary matrix
For hash reuse, preserve the distinction between a test that proves an honest peer rejects duplicates and a characterization test that demonstrates a malicious preimage holder can still take a reused-hash payment. The latter must not be presented as a fixed security property merely because the test passes.
Test sources to port or adapt
At the pinned Beignet test directory:
ffor-variant-d-vectors.test.ts,ffor-variant-d-setup.test.ts,ffor-variant-d-settlement.test.ts.ffor-variant-d-networking.test.ts,ffor-variant-d-m8.test.ts,ffor-variant-d-quorum.test.ts.ffor-adversarial-safety.test.ts,ffor-adversarial-recovery.test.ts,ffor-settle-policy.test.ts.ffor-witness-manifest.test.ts,ffor-witness-store.test.ts,ffor-witness-transport.test.ts.ffor-witness-barrier.test.ts,ffor-witness-crash.test.ts,ffor-witness-paging.test.ts.ffor-variant-d-hash-reuse.test.ts,ffor-witness-reuse.test.ts,ffor-issuer.test.ts.interop/ffor-variant-d-regtest.test.ts.tests/cli/ffor-surface.test.tsandtests/cli/daemon-options.test.tswhere applicable.Rust and integration qualification
Run and attach results for the actual implementation revision, using the repository's existing infrastructure setup and required environment variables:
Add the new named FFOR test targets, fuzz commands and platform jobs when implemented. On macOS raise the file-descriptor limit as documented before infrastructure tests. Run Miri on compatible targeted Rust tests where it can validate unsafe/ownership concerns, and record unsupported cases accurately. Review warnings rather than declaring a clean build without captured output.
Exit gate: executed cross-engine, storage, on-chain and device evidence at pinned revisions, including the negative cases. A green Beignet suite alone does not qualify this port.
16. Milestone M10: optional BOLT 12 issuer and reusable offer experience
This milestone is required if the release promises that unknown payers can request fresh invoices while Bitkit remains offline. It may ship after the fixed-invoice release, but must not be implied by it.
17. Rollout, documentation and completion criteria
Rollout
unbarrieredrecords, recoveries, reserved liquidity and force-close outcomes without logging secrets or sensitive payment payloads.Documentation
Definition of done
Closing this tracker requires links to the implementation PRs, exact tested artifact versions, CI runs, interoperability results and Android/iOS acceptance evidence. Creating the APIs or demonstrating only the happy path is insufficient.