Conversation
The client and server need to derive the same asset HTLC. Add a shared kit that preserves the legacy scripts, keys, and packet layout, and require callers to select the contract policy explicitly. Select the input from the proof's outpoint, check its anchor and the PSBT's prevout metadata, and verify the returned signature before constructing a witness. Tests pin the legacy vectors and exercise success and timeout spends.
Add the legacy two-party asset deposit kit to Loop so client and server can share address construction, proof checks, and timeout signing. Add the OP_TRUE virtual-packet helper alongside it. For timeout spends, find the input that spends the proof's outpoint, require prevout metadata for every input, and check the asset anchor and returned signature. This removes the assumption that the deposit is the first input.
Reject deposit delays outside BIP68's block-based range so sequence flags cannot change or disable the timeout. Build sweep outputs from the destination address, normalize OP_TRUE script keys, and fill every input witness before validating the virtual transfer. Reject unsupported address versions and duplicate anchor inputs. Tests cover multiple inputs and the resulting output layout.
A cooperative MuSig2 spend needs the output's full Taproot root, including the timeout sibling. Return that root so the spend can reproduce the funding output's tweak. Add separate helpers for checking a commitment against an expected amount and for asking tapd to verify a full proof file against an expected outpoint. Keep the old VerifyProof entry point as a deprecated wrapper.
Update Taproot Assets to v0.8.3 and taprpc to v1.3.3. Align the LND dependency at v0.21.3-beta, raise the Go build requirement to 1.25.13, and refresh the main and client RPC module dependencies. Add regression tests at the shared proof helpers to check that proofs with missing keys return errors instead of panicking.
Define the reservation purchase flow, its costs, and its acceptance criteria before building asset Loop Out on top. The plan uses held prepays, durable payment and funding records, and recovery through named FSM actions. Lay out the implementation as small commits and keep the previous prototype's databases outside this experiment.
Define quote, status, list, proof, and unpaid-cancellation calls for asset reservations. A client-generated ID identifies the same purchase across retries, including before its funding outpoint exists. Specify authenticated ownership, immutable quote terms, payment details, and public status fields. Generate the protobuf bindings and document the purchase API.
Define a reservation's quoted asset amount, fee, confirmation depth, and usable lifetime. Validate positive amounts, storage bounds, overflow, and block-based CSV limits. Check that the quoted lifetime values fit together without imposing a fixed fee or minimum lifetime policy. Test structural boundaries; approval and persistence follow in later commits.
Store a reservation's agreed terms separately from its ordered state history. Constrain identifiers, amounts, fees, and lifetime values, and make repeated inserts preserve the existing purchase. Allow a purchase row to exist before the server supplies its quote, with the fee and lifetime unset. Require those terms to form a complete set once supplied. Add schema tests for rollback, duplicate IDs, quoted fees, and invalid records. The shared database test helper selects SQLite or PostgreSQL through the existing build tag.
Add a typed SQL store that saves a purchase and its first state in one transaction. An identical create request returns the saved progress; a reused ID with different terms or keys fails. Updates preserve the agreed terms. Use the SQLite and PostgreSQL database abstraction and test concurrent creation, failed writes, invalid records, and reopening a SQLite database.
Calculate the timeout and execution cutoff from the first funding confirmation so extra confirmations and restarts cannot extend a reservation's life. Test deadline calculations and signed-height boundaries. Keep lifetime arithmetic separate from proof, confirmation, and spend checks.
Store the quote, main probe, SkipProbe preference, prepay routing cap, payment identity and result, outpoint, credit, and proof. Read the agreed amounts from the saved quote. Enforce unique funding outpoints and test the transition from an unquoted row.
Define separate Buy and Approve calls so callers can inspect the quote and main probe before authorizing payment. Buy can request skipped probing; Approve separately permits buying without a successful probe. Approving the exact quote hash accepts its asset fee and BTC prepay amount. The caller supplies the prepay routing cap and SkipProbe choice, which are saved with the transition that authorizes payment. Add get, list, probe-retry, and cancellation calls with purchase-ID and outpoint selectors. Report the probe outcome and estimated routing fees, assign swap read and execute permissions, and generate the bindings.
Retain the immutable quote, main probe, fee estimates, skip preference, prepay routing cap, payment identity, and delivery facts. Save progress and state history in one transaction, and prevent changes to payment choices once the purchase leaves approval. Check the quote hash, routing cap, and probe choice on approval. The hash binds consent to both quoted amounts. Read those amounts from the quote and payment choices from the reservation during recovery. Require a successful main probe unless the caller permits buying without one. Estimate prepay routing fees from the main fee and BTC amount ratio, rounding up and ignoring fixed hop fees. Test persistence, conflicting updates, quote consent, routing caps, outpoints, and fee arithmetic.
Coordinate quote requests, one main-payment probe, approval, prepay payment, and reservation verification through a persistent state machine. Save the probe result and estimate the prepay routing fee from it. Honor a saved skip request; an explicit probe retry clears it. Save the prepay routing cap and SkipProbe choice with the transition to PayPrepay. That state records consent. If the write fails, recovery waits for approval; if it succeeds, recovery uses the saved choices. Save the exact payment plan before sending and look up the original payment to resolve an uncertain result. If settlement wins a cancellation race, continue delivery. Verify the proof and funding before Ready, retain the original CSV clock, and stop node calls after a failed database write. Test recovery, consent, payment limits, and failed writes with SQL and node doubles.
Restore unfinished reservations and start one FSM worker for each. Route approval, cancellation, probe retries, and wake requests through that worker so its actions run in sequence. Periodic OnRecover events resume purchases and monitor ready reservations until expiry. Before starting a new worker, save the reservation ID, client key, requested asset and amount, and SkipProbe choice. Repeated creation requests return the saved reservation; conflicting assets or amounts are rejected. Apply MaxActiveReservations only to new purchases, so lowering the limit cannot prevent recovery. Keep one pending command per worker and return ErrReservationBusy when its queue is full. Bound worker attempts with CallTimeout, return saved snapshots to callers, and cancel and join workers on shutdown. Creation and its snapshot read share one timeout and stop when either the caller or the manager cancels. Test concurrent duplicate requests, recovery above MaxActiveReservations, shutdown during quote retrieval, key derivation, and snapshot reads, and SkipProbe across restarts.
Bind the deposit proof to its amount, scripts, outpoint, and local co-signing key before calling tapd. Record verified issuance metadata so tapd v0.8.3 can verify assets new to the client, then verify the full history. This does not import wallet assets. Check the proof's block against LND and observe the exact confirmed output. Use LND confirmation, spend, and block notifications to watch deposits and restore watches after restart. As with Instant Out, Ready means no spend is known; it does not wait for a synchronous unspent-output check. Test proof binding, issuance insertion before history verification, rejected proofs, repeated verification, and watch recovery.
Add the serialized receiving RFQ to the reservation quote. The client needs it to check that the prepay invoice's BTC amount and route hint match the agreed asset fee and conversion edge before approval. Carry the public PeerAcceptedBuyQuote and regenerate the protobuf binding.
Calculate a BTC invoice amount that converts to exactly the requested asset units, and reject rates or RFQ capacities that cannot carry it. Bind the signed invoice to the asset, payee, payment secret, receiving edge, and expiry. Validate each live HTLC's identity and asset records, then require their total to equal the agreed amount. Return every part's expiry for settlement checks. Keep receipt verification tied to the original quote so an expired quote does not invalidate an earlier settlement.
Add the LND payment adapter for reservation purchases. Validate both invoices and probe the estimated main payment through the invoice-based fee estimator. Prepare the prepay plan for the FSM to save before sending it. Enforce approved fees, payment lifetime, route CLTV limits, and enough BTC in one channel after its reserve, even when probing is skipped. Send one part to avoid conversion rounding across shards. Resolve uncertain results by tracking the saved hash on the saved node, and validate the settled amount and routing fee.
Connect the purchase manager to loopd's database, configured tapd, server connection, and authenticated LND services. Start recovery during daemon initialization and wait for saved purchases before serving calls. Use the proof adapter to record issuance metadata before verifying reservation proofs through tapd. Implement quote approval, outpoint selection, and paginated status. Pass the skip preference on quote and approval requests to the manager. Enable purchases when experimental features and tapd are active. Test separate approval, explicit consent to skip probing, and disabled- service responses.
Add buy, list, get, probe, and cancel under loop asset reservation. Print the purchase ID before dispatch so a lost reply can be retried, and accept canonical outpoints once funding is known. Show main-payment reachability, the quote, and the routing cap before approval. Display the full prepaid service fee in asset units and as a percentage, including the transport minimum that enables smaller swaps. Document that the fee is fully charged and the main payment remains the principal. Estimate prepay routing fees from the main probe. Keep the conventional routing limit unless the caller sets another limit. Normal purchases require a successful main probe. The --skip_probe flag skips probing for new purchases and accepts a failed result on resume. It preserves quote approval and payment limits; --yes only skips the prompt. After approval, wait for verified delivery while the daemon continues recovery.
Record the September 10 real-node purchase run with tapd v0.8.3, LND v0.21.3-beta, and a SQLite client. It covered a BTC-only payer, both probes, daemon restarts, proof rejection, unfundable-purchase cancellation, and the 1,440-block CSV sweep. Mark that run as predating the current single-probe flow. List the remaining node-failure tests and client database and backend variants. Asset swap execution remains outside this case.
hieblmi
force-pushed
the
hai/asset-reservations-stepwise
branch
from
September 16, 2026 13:30
8d8a932 to
eecb709
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
This PR lets a client buy and manage an asset reservation before starting a
swap. A reservation sets aside a fixed asset amount in an on-chain output
for that client, for a limited time. Spending uses a shared client/server key;
a timeout path allows unused funds to be reclaimed.
The client pays the quoted service fee upfront to obtain the reservation.
The asset principal is paid for later, when the client executes the swap.
Under this contract, the prepayment covers the later swap's service fee and
is credited once. Routing and miner fees are separate, and the prepayment is
not refunded if the reservation goes unused.
The purchase flow validates and saves a quote, checks whether the estimated
main payment can reach its destination, asks for approval, pays the BTC prepay
invoice, and verifies the delivered reservation before marking it
Ready.A successful probe reports current reachability; it does not reserve Lightning
liquidity for the future swap. The prepay routing-fee estimate is derived from
the main probe, so purchasing requires only one probe.
Purchase flow
flowchart TD Q["RequestQuote: request and validate a quote<br/>Save the purchase ID, keys, and agreed terms"] P["ProbeRoutes: probe the estimated main payment<br/>Save reachability and routing-fee estimates"] A["AwaitApproval: show the quote and probe result"] PAY["PayPrepay: save payment intent<br/>Pay and track the BTC prepay invoice"] WAIT["WaitForDelivery: obtain the funding outpoint and asset proof"] VERIFY["VerifyReservation: check the asset, amount, keys, proof,<br/>confirmations, and remaining lifetime"] READY["Ready: verified reservation available for a later swap"] CANCEL["CancelPrepay: resolve any payment before cancellation"] CANCELED["Canceled"] Q --> P Q -->|Explicit --skip_probe| A P --> A A -->|Approve exact quote and routing cap| PAY A -->|Retry probe| P A -->|Cancel or quote expires| CANCEL PAY -->|Payment settled| WAIT WAIT --> VERIFY VERIFY -->|Checks pass| READY CANCEL -->|Unpaid cancellation confirmed| CANCELED CANCEL -->|Payment settled instead| WAITNormal approval requires a successful main-payment probe.
--skip_probeexplicitly permits purchase without one; quote validation and payment limits
still apply. Declining the prompt leaves the purchase saved and unpaid.
The daemon saves progress throughout the purchase and resumes unfinished work
after a restart. It records the exact payment before sending and resolves an
uncertain result by tracking that payment. Delivery is verified locally, and
ready reservations remain monitored for spending and expiry. Recovery retains
the original lifetime rather than starting a new clock.
Using the reservation in a swap
A later asset Loop Out will select the funded reservation by its
txid:vout,check that enough time remains, and obtain a fresh execution quote. The client
will pay the main Lightning invoice to receive the reserved assets on-chain
through the swap protocol. The saved prepay credit avoids charging the service
fee twice; the BTC-to-asset conversion price can change before execution and
requires fresh approval.
This PR implements reservation purchase through
Ready. Executing the laterasset swap is follow-up work. It includes persistent terms and state history,
payment recovery, proof and confirmation checks, authenticated RPCs, and
loop asset reservation buy,list,get,probe, andcancel. Purchasesrequire
--experimentaland a configured tapd.Extending this to BTC reservations
The same quote, approval, prepay, recovery, and readiness flow can support BTC
reservations. A follow-up would add BTC-denominated terms and a Bitcoin funding
verifier in place of asset proofs and conversion checks, then connect ready
reservations to Instant Out. BTC reservation purchases are not enabled here.
Earlier work and dependency
This replaces the reservation-buying approach in:
This branch includes the shared asset tooling from
#1225. The reservation-specific
commits begin after
70ec06b9; the prerequisite remains in the diff until theshared tooling is merged and this branch is rebased.
Validation
go test ./assets/reservation ./assets/payment ./loopdb ./loopd ./cmd/loop -count=1go test ./... -count=1in bothlooprpcandswapserverrpcgo test -race ./assets/reservation -run '^TestClientManagerCancelsCreation$' -count=1make commitmsg-lint range=70ec06b9..HEADgit diff --check 70ec06b9..HEADThe documented real-node purchase run predates the current single-probe flow.
A fresh real-node run of that flow, broader node-failure testing, and the
PostgreSQL/Neutrino variants remain outstanding. Reorg recovery remains
follow-up work; this feature is experimental.
Pull Request Checklist
docs/release-notes/release-notes-next.md.