Skip to content

assets/reservation: Add client-initiated reservation purchases - #1229

Open
hieblmi wants to merge 30 commits into
lightninglabs:masterfrom
hieblmi:hai/asset-reservations-stepwise
Open

hieblmi wants to merge 30 commits into
lightninglabs:masterfrom
hieblmi:hai/asset-reservations-stepwise

Conversation

@hieblmi

@hieblmi hieblmi commented Sep 15, 2026 •

Copy link
Copy Markdown
Collaborator

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.

A buyer selects either an exact asset ID or a compressed, tweaked group key. An exact request requires that issuance both on chain and on the receiving channel. A group request allows different fungible issuances for the two legs. The quote identifies both; loopd verifies the anchor and member issuance proofs before it probes or pays. The buyer approves those exact terms, and delivery must prove the quoted funding issuance.

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, sends one full-amount asset
hold probe through SendPaymentV2, asks for approval, pays the BTC prepay
invoice, and verifies delivery before marking the reservation Ready.
The server validates the accepted asset HTLC and saves its receipt before
canceling the probe invoice. Success requires that receipt, server cancellation,
and a failed local payment with FAILURE_REASON_INCORRECT_PAYMENT_DETAILS.
Success does not depend on LND retaining failed HTLC attempts.

Each purchase has one quote and one probe payment. Failed probes cancel the
unpaid purchase; another attempt requires a fresh reservation ID, optionally
with --skip_probe. Recovery resumes the saved payment or finishes cancellation.
The probe checks current liquidity without reserving it for the later swap.
The client captures route fees from live SendPaymentV2 updates and resumes
observation through TrackPaymentV2. The prepay estimate scales the observed
route fee by the invoice amounts. If recovery misses the route before LND
prunes it, the probe can still succeed with unavailable fee estimates;
probe_fee_known distinguishes missing data from an observed zero fee.

Purchase flow

flowchart TD
    Q["RequestQuote: request and validate a quote<br/>Save the purchase ID, keys, and agreed terms"]
    P["ProbeRoutes: send the full-amount hold probe<br/>Verify receipt and cancellation"]
    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 -->|Verified receipt and resolved payment| A
    P -->|Probe failed| CANCEL
    A -->|Approve exact quote and routing cap| PAY
    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| WAIT
Loading

Normal approval requires a successful main-payment probe. --skip_probe
explicitly 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 later
asset 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, and get. Purchases
require --experimental and 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
#883 and
#1195.

The branch stacks on Loop #1225 at 0ad07d71c and uses tap-sdk v0.1.1 with Taproot Assets v0.8.8 and the TLV decoder from that tag. Purchases retain their negotiated, server-funded CSV contract. The shared deposit constructor supplies the anchor and refund leaf, and the kit ties the agreed terms to an isolated asset commitment. New on-chain reservations require a running tapd v0.8.8 or later. The server integration is Nautilus #1730.

The paying node can retry a transient identity lookup before probe dispatch until the original quote expires. If local verification invalidates a delivered proof after a reorg, the client retains the delivery record, leaves Ready and withdraws proof verification and execution deadlines.

Validation

Before this minimum-version update, the full Loop unit suite, affected race suites, lint and module tidy checks passed. The real-node purchase case exercised quote, probe, held prepay, funding, client/server restart, bound proof acceptance, cancellation and the CSV refund.

At the new tapd v0.8.8 minimum, the policy, swapkit, reservation and loopd unit tests pass. The boundary tests cover both the released daemon and rejected older nodes. The v0.8.8 release fixes a regression that could prevent asset invoices from settling. Taproot Assets v0.8.8 release notes

Pull Request Checklist

  • Added entries to docs/release-notes/release-notes-next.md.

@hieblmi hieblmi self-assigned this Sep 15, 2026
@hieblmi hieblmi added the asset loop out Taproot Asset Loop Out work label Sep 15, 2026
@hieblmi
hieblmi force-pushed the hai/asset-reservations-stepwise branch 3 times, most recently from 1681b08 to 7ed9ffd Compare September 23, 2026 12:44
@hieblmi
hieblmi force-pushed the hai/asset-reservations-stepwise branch 5 times, most recently from 55ea4d1 to 4a906f8 Compare September 24, 2026 10:56
@hieblmi
hieblmi force-pushed the hai/asset-reservations-stepwise branch 4 times, most recently from 47f09e2 to f828adc Compare October 27, 2026 15:12
Route existing HTLC, static address and reservation scripts through
the same leaf serializers. Pin the quote templates so moving the
serialization cannot change signed numbers or zero-hash fee estimates.
Define the key roles, cooperative and unilateral paths, witnesses and
weight estimates for both directions. Exercise the legacy vectors and
execute the spend conditions against the Bitcoin script engine.
@hieblmi
hieblmi force-pushed the hai/asset-reservations-stepwise branch 3 times, most recently from b20b34a to b8fcda0 Compare November 3, 2026 16:04
Build the swapkit adapter against the released pre-btcd-v2 modules.
Pin tapd's bounded legacy TLV decoder at the v0.8.8 release commit.
Map contracts and asset descriptors into SDK plans and reuse the
authenticated wallet connection.
Taproot Assets v0.8.8 fixes an asset-invoice settlement regression.
Check the running backend before new on-chain operations. Verify proof
history, active and passive holdings, Bitcoin anchors and fee prevouts.
Keep offline transaction and witness construction available.
Prepare confirmed inputs for native Build and check its allocations,
Bitcoin template, change policy and fee before granting authority.
Verify wallet-funded fees again after native Commit.
Check signing policy and the saved package before using LND, and
accept peer witnesses only for the approved transaction. Preserve
durable recovery while ephemeral signing sessions remain caller-owned.
Bind native compact paths to approved signed parents. After parents
confirm, replace proof metadata with authenticated histories without
altering the transaction or final witnesses.
Reconcile fallback, preimage and refund spends against agreed terms.
Recover and export exact proofs, including after ambiguous publication
or a restart. Verify against the transaction that actually mined.
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.

Persist the sole hold probe request, paying node, deadline, and terminal
result alongside the reservation purchase. Recovery resumes that payment
without replacing the quote or registering another probe.

Persist whether a live probe route supplied the fee estimate, so
recovery distinguishes missing route history from a zero-fee route.
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 and list 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. Cancellation stays
internal; unapproved quotes expire without a public cancellation RPC.

Allow list requests to select one client state or active purchases. The
filters are mutually exclusive; omitting both includes all states.

Expose probe fee availability separately from probe success. Recovery
can verify delivery even if LND has already pruned the route history.
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.

Bind the probe hash to the client-generated reservation ID. Preserve the
probe request, node, deadline, and terminal result across writes and
restarts; the purchase keeps one quote and one probe.

Save live route fee availability with purchase progress, including an
observed zero fee. Missing attempt history does not imply a zero fee.
Drive quote requests, one full-amount hold probe, approval, prepay
payment, and reservation verification through a persistent state
machine. Save the probe request before dispatch. Require server receipt
and cancellation plus a resolved local payment before approval.

A failed probe cancels the unpaid purchase. Resolve any in-flight HTLC
before final cancellation; a fresh attempt needs a new purchase ID.
Recovery resumes saved work and recognizes completed probes even after
their deadline, while still enforcing quote expiry.

Save approval and routing limits before sending the prepay, and track
the original payment after uncertain results. Settlement always resumes
delivery. Verify the proof and funding before Ready, retain the original
CSV clock, and stop node calls after a failed database write.

Persist fees from live payment updates. Recognize probe success from the
payment-level incorrect-details reason and server receipt and
cancellation, without requiring terminal HTLC attempt records. A missed
live route leaves the estimate unavailable but does not fail delivery.
Restore unfinished reservations and start one FSM worker for each. Route
approval 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, automatic expiry of unapproved quotes, and SkipProbe
across restarts.

Filter startup recovery in SQL to exclude canceled and expired
purchases. Read matching reservations and their latest state and
timestamp in one statement, avoiding full history reads and mixed
PostgreSQL snapshots. Support either one named state or active purchases
in manager list calls. Test filtering, single-statement reads, and
recovery past completed records.
Bind the deposit proof to its amount, scripts, outpoint, and local
co-signing key before calling tapd. Record verified issuance metadata
for assets new to the tapd v0.8.8 verifier, 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.
Carry the native receiving RFQs for the prepay and full-amount probe so
the client can validate exact asset amounts, invoice terms, and route
hints. Report probe receipt and invoice cancellation separately.

Put probe-only cancellation in a dedicated cancellation request, keeping
it separate from reservation identity. Regenerate the protobuf bindings.
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.
Validate both receiving RFQs and invoices. Prepare one full-amount hold
probe for the FSM to save, then dispatch it with SendPaymentV2. Keep the
SendPaymentV2 stream open to capture in-flight routes before LND prunes
failed attempts. Resume observation with TrackPaymentV2 on the saved
node, passing live updates to the FSM for immediate persistence. Bound
streams by the saved deadline and recheck it before dispatch.

Enforce approved prepay fees, payment lifetime, route CLTV limits, and
enough BTC in one channel after its reserve, even when probing is
skipped. Send one part to preserve exact asset conversion. Resolve
uncertain payments by tracking their saved identity 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.

Pass the list state or active-only filter to the manager before
pagination. Reject conflicting filters and unknown state names, and
preserve stable ID pagination for filtered results.

Expose fee availability only for successful probes with a saved live
route observation.
Allow the proof RPC to receive the existing 16 MiB proof limit plus
response metadata. Share the raw proof bound across verification
adapters and cover large and oversized responses through real gRPC
calls.
Add buy, list, and get under loop asset reservation. Probing and
cancellation stay internal; unapproved purchases expire automatically.
Print the purchase ID before dispatch so a lost reply can be resumed.
Accept canonical outpoints once funding is known.

Display the full-amount probe result, quoted asset service fee, and
routing cap before approval. Estimate prepay routing fees from the main
probe and keep the conventional routing limit unless overridden.

Failed probes cancel the unpaid purchase; another attempt requires a
fresh purchase ID. The --skip_probe flag allows a new purchase without a
successful probe. It preserves quote approval and payment limits; --yes
only skips the prompt. After approval, wait for verified delivery while
the daemon continues recovery.

Support mutually exclusive --state and --active_only list filters.
Preserve filters across pages; active-only excludes Canceled and Expired
while retaining Ready and NeedAdminAttention.

Show unavailable fee estimates separately from observed zero fees. A
successful probe remains eligible for approval without a captured route.
Document one full-amount hold probe per quote, verified asset receipt,
cancellation, and recovery of the original payment. Failed probes cancel
the unpaid purchase; retrying requires a fresh reservation ID.

Distinguish the early prototype runs from the Taproot Assets v0.8.8
reservation itests. Describe the BTC-only payer, restart recovery and
the 144-block CSV sweep. Swap execution is a separate operation.

Document live stream fee capture and success with pruned HTLC attempts.
Recovery without a route reports unavailable estimates; retaining failed
payment attempts in LND is not required.
End quote acquisition after one minute across restarts, and stop invalid
quotes before any payment. Preserve the existing OutOfRange outcome for
funding refusals separately from unavailable or invalid quotes.

Display the exact BTC prepay and implied prepay/probe prices before
approval. Reject RFQ rates more than five percent apart without treating
server-supplied rates as an independent market price.
Use tap-sdk v0.1.1 with Taproot Assets v0.8.8. Bind purchases to the
shared CSV contract and fixed asset descriptor. Require an isolated
asset commitment and bind the verified proof to the quoted issuance,
amount, script key and funding outpoint. Keep node failures retryable.

Expose contract signing before wallet fee input finalization so the
server can recover a reservation with the same SDK signing policy.
@hieblmi
hieblmi force-pushed the hai/asset-reservations-stepwise branch 2 times, most recently from f1241fd to 96aaced Compare November 4, 2026 13:49
Keep the original exact-ID protocol. For a group request bind the
funding and Lightning issuances in one immutable quote. Verify both
genesis proofs before probing and verify the actual funding output
before marking the reservation ready.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

asset loop out Taproot Asset Loop Out work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant