lightninglabs / lightninglabs/taproot-assets
tapfreighter: failed anchor publication can strand a transfer
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 525
- Forks
- 150
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 31
Description
(N.b., this describes the issue underlying #1348 and a proposed alternative fix that assumes the watcher architecture introduced in e.g. #2266 and #2287.
The issue below has been drafted by Sol, based on initial work by Fable. I think the most important idea is to enqueue anchor publication as an effect to be dispatched by the watcher outbox; this feels intuitively like the correct way to handle things.)
## Summary
The chain porter makes a transfer durable before publishing its anchor transaction. It atomically registers the porter anchoring, stores the pending transfer and leases its asset inputs. This is the right commit boundary: once it succeeds, the watcher knows every local state change that depends on the transaction confirming.
The publication step does not yet respect that boundary. If lnd returns an error, the send call fails even though the transfer remains durable. Its inputs stay leased, the transaction is retried when the daemon restarts, and an error that remains stable can leave the transfer pending indefinitely.
The problem is not simply a missing unlock, but that a durable transfer has a fallible external act (publication) but that act is neither durably scheduled nor represented to the operator.
## Conceptual problem
A publication result and a chain outcome are different kinds of fact.
- A successful publication says that the local node accepted the transaction. It does not say that the transaction will confirm.
- A timeout says that the caller did not receive an answer. The transaction may nevertheless have been accepted.
- A policy or double-spend rejection describes the node's present mempool view. It does not prove which transaction will ultimately win.
- The watcher, by contrast, observes the chain and can decide whether the intended anchor was witnessed, buried or foreclosed by another spend.
For this reason, releasing the asset inputs on every publication error is unsafe. The anchor transaction may already be in the network and may still confirm. Making the same assets selectable again would create a second local claim that conflicts with the durable transfer.
Publication failure should therefore be treated as a failed act to retry and surface—not as a terminal judgment about the transfer.
## Proposed fix
### Make publication durable
When the porter registers a self-published transfer, enqueue an anchor publication effect in the same transaction that stores the transfer and its anchoring. The invariant should be:
> Every durable, self-published transfer has either evidence that its anchor was published or observed, or an outstanding obligation to publish it.
The watcher outbox should dispatch that effect idempotently and retry failures with persistent backoff. Publication should have one retry owner; a single outbox attempt should not contain another long retry loop.
The last error and retry state should be visible through transfer or anchoring status. A stable rejection then becomes an actionable pending condition rather than an invisible restart loop.
### Report the commit truthfully
Once the registration transaction commits, `SendAsset` should return the created transfer and anchor txid. A subsequent publication failure must not be reported as though the request left no durable state behind. Its publication status can advance asynchronously.
Failures before that commit boundary still return ordinary errors and leave no pending transfer.
### Let the watcher decide chain outcomes
The porter should continue to rely exclusively on its watcher site for chain-derived outcomes:
- `Witnessed` or `Buried`: the anchor is on chain, so any outstanding publication obligation can be discharged.
- `Conflicted`: retain the transfer and its leases while the competing spend remains reversible.
- `Abandoned`: the chain has decided against the transfer at the safe depth, so the existing porter compensation can run.
A publication error—including a reported double spend—must not bypass this progression and manufacture an `Abandoned` outcome.
### Add explicit porter abandonment
Some transactions will remain unresolved: for example, an anchor that is persistently rejected by local policy but has no conflicting spend on chain. Operators need a supported way to judge such a transfer dead.
Add a porter-level `AbandonTransfer` operation for an unresolved, unwitnessed transfer. It should withdraw the porter anchoring and apply the porter's compensation in one database transaction. The operation must:
- mark the transfer permanently abandoned so it cannot resume;
- cancel its outstanding publication and delivery obligations;
- restore only the asset inputs that can safely be restored;
- release the corresponding asset leases; and
- enqueue any external cleanup that cannot occur atomically.
This must be a site-aware porter operation. Withdrawing only the generic watcher record would leave the transfer's own state and leases behind.
Operator abandonment is an explicit judgment, not proof that the transaction is globally absent. The RPC and CLI should state that an unconfirmed transaction might still exist elsewhere and later confirm.
## Error classification
Publication errors should retain stable causes across the lnd gRPC boundary. This improves retry policy and diagnostics—for example, distinguishing transport failure, low-fee rejection, a conflicting spend and an already-known transaction.
Classification is not the source of terminal authority. No publication error should directly release the assets; terminality comes from the watcher or explicit operator abandonment.
## Reproduction
1. Persist a transfer immediately before its anchor transaction is published.
2. Raise the backend's minimum relay fee above the signed transaction's fee rate.
3. Allow publication to run or restart the daemon.
4. Observe that publication fails while the transfer remains pending and its inputs remain leased.
5. Restart again and observe the same attempt and failure.
## Acceptance criteria
- Registration atomically creates the pending transfer, porter anchoring, asset leases and publication obligation.
- Publication attempts and backoff survive restart without recreating a parcel driver or blocking startup.
- A publication error never automatically releases asset inputs or marks the transfer abandoned.
- Once registration commits, `SendAsset` returns the durable transfer even if publication subsequently fails.
- A transaction that was accepted before an ambiguous RPC failure can still be observed and completed normally.
- A buried competing spend abandons the transfer through the watcher and existing porter compensation.
- An operator can abandon an unresolved transfer through the porter; the terminal state, compensation and obligation cancellation are atomic, and the transfer does not resume afterward.
- Publication failures and their retry state are visible to operators.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing SendAsset through porter registration and the watcher outbox, then inspect the existing watcher outcomes and porter compensation paths. Review the RPC and CLI entry points for transfer status and abandonment. Done means durable publication retries and visible state, truthful post-commit SendAsset results, watcher-driven outcomes, and atomic operator abandonment without unsafe lease release.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, grpc
- Domain
- backend, cli, distributed-systems, payments
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100