lightninglabs / lightninglabs/taproot-assets
rpc: support multi-packet wallet funding for grouped multi-issuance transfers
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 525
- Forks
- 150
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 31
Description
Problem
FundVirtualPsbt can only ever return a single funded virtual packet. It hard-rejects any funding result that produced more than one packet. That is fine for a send that draws from a single asset ID/tranche, but it makes wallet-driven coin selection unusable for grouped assets: a group key that spans multiple tranches (multi-issuance) naturally selects inputs across several asset IDs, and the funding pipeline deliberately produces one virtual packet per asset ID. As a result, the wallet-selected path for grouped-group inputs is dead — the caller gets only one packet supported and cannot fund the transfer at all.
This matters specifically for lightweight/custom-anchor integrations that let tapd do coin selection (the caller owns the BTC anchor tx and just wants tapd to select and commit the assets). Anything that lets the wallet pick grouped inputs across tranches hits the guard. Only the explicit proof-selected/single-tranche case survives, because that collapses to a single asset ID and therefore a single packet.
Current behaviour
FundVirtualPsbt funds via AssetWallet.FundPacket (PSBT template path, rpcserver/rpcserver.go:2689) or AssetWallet.FundAddressSend (raw template path, rpcserver/rpcserver.go:2774). Both return a *tapfreighter.FundedVPacket, whose VPackets field is a slice (tapfreighter/wallet.go:250-262):
type FundedVPacket struct {
VPackets []*tappsbt.VPacket
...
}
That slice is populated by createFundedPacketWithInputs, which by design emits one packet per tranche. Its doc comment is explicit (tapfreighter/fund.go:22-25): "A new vPacket is created for each tranche of the active asset (which is ... a single asset ID/tranche or group key with multiple tranches)." The multi-packet fan-out happens in tapsend.DistributeCoins (tapfreighter/fund.go:91-97 → tapsend/allocation.go:439), which groups inputs by asset ID and creates one virtual packet per asset ID (tapsend/allocation.go:505-508, via GroupProofsByAssetID). The result is returned as FundedVPacket{VPackets: allPackets, ...} at tapfreighter/fund.go:151-155.
Grouped selection across multiple asset IDs is a first-class funding mode: FundingDescriptor.DistinctSpecifier (tapsend/send.go:188-192) says that when the group key is set, "we ignore the asset ID and allow multiple inputs of the same group to be selected." So a grouped multi-issuance send is expected to select inputs from several asset IDs, which DistributeCoins then splits into several packets.
Despite FundedVPacket.VPackets being a slice, the RPC layer collapses it to one and rejects the rest (rpcserver/rpcserver.go:2811-2819):
// TODO(guggero): Remove this once we support multiple packets.
if len(fundedVPkt.VPackets) > 1 {
return nil, fmt.Errorf("only one packet supported")
}
response.FundedPsbt, err = fn.Serialize(fundedVPkt.VPackets[0])
The response message itself is single-packet shaped (taprpc/assetwalletrpc/assetwallet.proto:173-198): bytes funded_psbt = 1 plus a single int32 change_output_index = 2 (which is in fact hardcoded to 0 at rpcserver/rpcserver.go:2799, never recomputed per packet). Notably, the downstream RPCs that consume these packets are already multi-packet aware: both AnchorVirtualPsbtsRequest and CommitVirtualPsbtsRequest accept repeated bytes virtual_psbts (taprpc/assetwalletrpc/assetwallet.proto:271-287). So the only place that still assumes a single packet is the FundVirtualPsbt response and its guard.
Proposed change
Let FundVirtualPsbt return all funded packets and drop the guard:
- Extend
FundVirtualPsbtResponsewith a repeated funded-packet field, e.g.repeated bytes funded_psbts(and make change-output reporting per-packet, e.g.repeated int32 change_output_indexes, since each tranche packet can carry its own change output). Keepbytes funded_psbt/int32 change_output_indexpopulated for the single-packet case to preserve backward compatibility for existing callers, and populate them only whenlen(VPackets) == 1. - Remove the
len(fundedVPkt.VPackets) > 1rejection atrpcserver/rpcserver.go:2811-2814and serialize every entry offundedVPkt.VPacketsinto the new repeated field instead of onlyVPackets[0]atrpcserver/rpcserver.go:2816. - Compute the change output index per packet rather than hardcoding
0(rpcserver/rpcserver.go:2799); eachVPacketknows its split-root/change output.
The passive-asset handling (rpcserver/rpcserver.go:2789-2809) already operates over fundedVPkt.VPackets as a set, so it does not need to change. The consuming RPCs (AnchorVirtualPsbts, CommitVirtualPsbts) already take repeated bytes virtual_psbts, so no downstream RPC surface change is required — a caller would simply forward the full list. Client tooling (e.g. tapcli and the itest helpers in itest/psbt_test.go, itest/multi_send_test.go) that reads funded_psbt should be updated to iterate the repeated field.
If any part of the sign/commit flow still assumes one packet per fund call, that assumption should be located and lifted as part of this change; the wallet layer (FundedVPacket.VPackets, DistributeCoins) already supports N packets, so the remaining work is concentrated at the RPC boundary. The exact tapcli call sites that read the singular field need confirmation.
Context
Surfaced by lightninglabs/tap-sdk#158 (advanced custom-anchor Taproot Assets tx builder, used by SwapDK). The SDK fails closed here: it only supports the proof-selected/single-tranche MVP because that reliably yields exactly one packet, and it cannot offer wallet-driven coin selection for grouped multi-issuance assets while FundVirtualPsbt rejects multi-packet results (rpcserver/rpcserver.go:2811-2814). Lifting this guard (and widening the response) lets the SDK hand grouped coin selection to tapd instead of requiring callers to pre-select proofs per tranche.
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 at FundVirtualPsbt in rpcserver/rpcserver.go, then inspect FundedVPacket in tapfreighter/wallet.go and the FundVirtualPsbtResponse definition in taprpc/assetwalletrpc/assetwallet.proto. Trace packet creation through tapfreighter/fund.go and tapsend/allocation.go, and check tapcli plus itest/psbt_test.go and itest/multi_send_test.go for singular-field consumers. Done means grouped multi-issuance funding returns all packets, preserves the single-packet fields, reports per-packet change indexes, and callers can forward the repeated list.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100