Skip to content

Commit f1f595a

Browse files
author
ffranr
authored
Merge pull request #1505 from lightninglabs/coop-close-allocation-sort
channels: fix coop close asset distribution for grouped asset channels
2 parents e3862cc + 83823c5 commit f1f595a

File tree

3 files changed

+187
-16
lines changed

3 files changed

+187
-16
lines changed

tapchannel/aux_closer.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -402,13 +402,43 @@ func (a *AuxChanCloser) AuxCloseOutputs(
402402
closeAllocs[idx].OutputIndex = uint32(idx)
403403
}
404404

405-
// Now that we have the complete set of allocations, we'll distribute
406-
// them to create the vPackets we'll need to anchor everything.
407-
vPackets, err := tapsend.DistributeCoins(
408-
inputProofs, closeAllocs, a.cfg.ChainParams, true, tappsbt.V1,
405+
// Now we know the deterministic ordering of the local/remote asset/btc
406+
// outputs, we can extract the output indexes for the allocations.
407+
var (
408+
localOutputIndex, remoteOutputIndex uint32
409+
)
410+
if localAlloc != nil {
411+
localOutputIndex = localAlloc.OutputIndex
412+
}
413+
if remoteAlloc != nil {
414+
remoteOutputIndex = remoteAlloc.OutputIndex
415+
}
416+
417+
// We don't use the normal allocation code here. This requires a bit of
418+
// a lengthy explanation: When we close a channel, the output of the
419+
// `lncli closedchannels` command will show the last commitment state of
420+
// the channel as the closing asset balance. Which is correct in terms
421+
// of balances. But if there are multiple different asset IDs (e.g., in
422+
// a grouped asset channel), then _how_ those pieces are distributed
423+
// within the commitment transaction depends on the order of the
424+
// allocations. And the order of the allocations is dependent on the
425+
// BTC amount and the pkScript of the BTC-level output. Both of which
426+
// are different in the coop close output (we set the asset-level output
427+
// BTC amount to the dummy amount, and the pkScript will be a newly
428+
// derived internal key with no sibling script path).
429+
// So, long story short: If we used the tapsend.DistributeCoins method
430+
// here, it could happen that the actual asset output distribution shown
431+
// in the `lncli closedchannels` command would be different from the
432+
// actual distribution in the co-op close transaction.
433+
// This could mostly be seen as an UX-only issue, but was actually
434+
// discovered while attempting to assert the final closing balance of
435+
// grouped asset channels in the litd integration test.
436+
vPackets, err := CommitmentToPackets(
437+
commitState, inputProofs, a.cfg.ChainParams, localShutdown,
438+
remoteShutdown, localOutputIndex, remoteOutputIndex, tappsbt.V1,
409439
)
410440
if err != nil {
411-
return none, fmt.Errorf("unable to distribute coins: %w", err)
441+
return none, fmt.Errorf("unable to create vPackets: %w", err)
412442
}
413443

414444
// We can now add the witness for the OP_TRUE spend of the commitment

tapchannel/commitment.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"fmt"
7+
"net/url"
78
"sort"
89

910
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -28,6 +29,7 @@ import (
2829
"github.com/lightningnetwork/lnd/lnwallet"
2930
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
3031
"github.com/lightningnetwork/lnd/lnwire"
32+
"golang.org/x/exp/maps"
3133
)
3234

3335
// DecodedDescriptor is a wrapper around a PaymentDescriptor that also includes
@@ -1505,6 +1507,132 @@ func deriveFundingScriptKey(ctx context.Context, addrBook address.Storage,
15051507
return fundingScriptKey, nil
15061508
}
15071509

1510+
// CommitmentToPackets converts a commitment to a list of vPackets. The
1511+
// commitment must not contain any HTLCs, as this only works for coop-closed
1512+
// channels.
1513+
func CommitmentToPackets(c *cmsg.Commitment, inputs []*proof.Proof,
1514+
chainParams *address.ChainParams, localShutdownMsg,
1515+
remoteShutdownMsg cmsg.AuxShutdownMsg, localOutputIndex,
1516+
remoteOutputIndex uint32,
1517+
vPktVersion tappsbt.VPacketVersion) ([]*tappsbt.VPacket, error) {
1518+
1519+
if len(c.IncomingHtlcAssets.Val.HtlcOutputs) > 0 ||
1520+
len(c.OutgoingHtlcAssets.Val.HtlcOutputs) > 0 {
1521+
1522+
return nil, fmt.Errorf("commitment contains HTLCs, cannot " +
1523+
"create vPackets")
1524+
}
1525+
1526+
// We group the inputs by asset ID, so we can create a vPacket for each
1527+
// asset ID. The number of vPackets is dictated by the number of
1528+
// different asset IDs in the commitment transaction.
1529+
groupedInputs := tapsend.GroupProofsByAssetID(inputs)
1530+
vPackets := make(map[asset.ID]*tappsbt.VPacket, len(groupedInputs))
1531+
for assetID, proofsByID := range groupedInputs {
1532+
pkt, err := tappsbt.FromProofs(
1533+
proofsByID, chainParams, vPktVersion,
1534+
)
1535+
if err != nil {
1536+
return nil, fmt.Errorf("error creating vPacket: %w",
1537+
err)
1538+
}
1539+
1540+
vPackets[assetID] = pkt
1541+
}
1542+
1543+
localOutputs := c.LocalOutputs()
1544+
remoteOutputs := c.RemoteOutputs()
1545+
1546+
// We now distribute the outputs to the vPackets.
1547+
for _, output := range localOutputs {
1548+
pkt, ok := vPackets[output.AssetID.Val]
1549+
if !ok {
1550+
return nil, fmt.Errorf("no vPacket found "+
1551+
"for asset ID %s", output.AssetID.Val)
1552+
}
1553+
1554+
outAsset := output.Proof.Val.Asset
1555+
vOut, err := assetToInteractiveVOutput(
1556+
outAsset, asset.V0, localShutdownMsg,
1557+
localOutputIndex,
1558+
)
1559+
if err != nil {
1560+
return nil, fmt.Errorf("error creating "+
1561+
"vOutput: %w", err)
1562+
}
1563+
1564+
pkt.Outputs = append(pkt.Outputs, vOut)
1565+
}
1566+
1567+
for _, output := range remoteOutputs {
1568+
pkt, ok := vPackets[output.AssetID.Val]
1569+
if !ok {
1570+
return nil, fmt.Errorf("no vPacket found "+
1571+
"for asset ID %s", output.AssetID.Val)
1572+
}
1573+
1574+
outAsset := output.Proof.Val.Asset
1575+
vOut, err := assetToInteractiveVOutput(
1576+
outAsset, asset.V0, remoteShutdownMsg,
1577+
remoteOutputIndex,
1578+
)
1579+
if err != nil {
1580+
return nil, fmt.Errorf("error creating "+
1581+
"vOutput: %w", err)
1582+
}
1583+
1584+
pkt.Outputs = append(pkt.Outputs, vOut)
1585+
}
1586+
1587+
return maps.Values(vPackets), nil
1588+
}
1589+
1590+
// assetToInteractiveVOutput creates a VOutput for an asset that is part of an
1591+
// interactive transaction.
1592+
func assetToInteractiveVOutput(a asset.Asset, version asset.Version,
1593+
shutdownMsg cmsg.AuxShutdownMsg,
1594+
anchorOutputIndex uint32) (*tappsbt.VOutput, error) {
1595+
1596+
scriptKey, ok := shutdownMsg.ScriptKeys.Val[a.ID()]
1597+
if !ok {
1598+
return nil, fmt.Errorf("no script key for asset %s", a.ID())
1599+
}
1600+
1601+
proofDeliveryUrl, err := lfn.MapOptionZ(
1602+
shutdownMsg.ProofDeliveryAddr.ValOpt(),
1603+
func(u []byte) lfn.Result[*url.URL] {
1604+
proofDeliveryUrl, err := url.Parse(string(u))
1605+
if err != nil {
1606+
return lfn.Err[*url.URL](fmt.Errorf("unable "+
1607+
"to parse proof delivery address: %w",
1608+
err))
1609+
}
1610+
1611+
return lfn.Ok(proofDeliveryUrl)
1612+
},
1613+
).Unpack()
1614+
if err != nil {
1615+
return nil, fmt.Errorf("unable to decode proof delivery "+
1616+
"address: %w", err)
1617+
}
1618+
1619+
outType := tappsbt.TypeSplitRoot
1620+
if a.SplitCommitmentRoot == nil {
1621+
outType = tappsbt.TypeSimple
1622+
}
1623+
1624+
return &tappsbt.VOutput{
1625+
Amount: a.Amount,
1626+
AssetVersion: version,
1627+
Type: outType,
1628+
Interactive: true,
1629+
AnchorOutputIndex: anchorOutputIndex,
1630+
AnchorOutputInternalKey: shutdownMsg.AssetInternalKey.Val,
1631+
ScriptKey: asset.NewScriptKey(&scriptKey),
1632+
ProofDeliveryAddress: proofDeliveryUrl,
1633+
}, nil
1634+
}
1635+
15081636
// InPlaceCustomCommitSort performs an in-place sort of a transaction, given a
15091637
// list of allocations. The sort is applied to the transaction outputs, using
15101638
// the allocation's OutputIndex. The transaction inputs are sorted by the

tapsend/allocation.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -486,19 +486,13 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation,
486486
// We group the assets by asset ID, since we'll want to create a single
487487
// virtual packet per asset ID (with each virtual packet potentially
488488
// having multiple inputs and outputs).
489-
assetIDs := fn.Map(inputs, func(input *proof.Proof) asset.ID {
490-
return input.Asset.ID()
491-
})
492-
uniqueAssetIDs := fn.NewSet(assetIDs...).ToSlice()
489+
groupedProofs := GroupProofsByAssetID(inputs)
493490

494491
// Each "piece" keeps track of how many assets of a specific asset ID
495492
// we have already distributed. The pieces are also the main way to
496493
// reference an asset ID's virtual packet.
497-
pieces := make([]*piece, len(uniqueAssetIDs))
498-
for i, assetID := range uniqueAssetIDs {
499-
proofsByID := fn.Filter(inputs, func(i *proof.Proof) bool {
500-
return i.Asset.ID() == assetID
501-
})
494+
pieces := make([]*piece, 0, len(groupedProofs))
495+
for assetID, proofsByID := range groupedProofs {
502496
sumByID := fn.Reduce(
503497
proofsByID, func(sum uint64, i *proof.Proof) uint64 {
504498
return sum + i.Asset.Amount
@@ -512,12 +506,12 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation,
512506
return nil, err
513507
}
514508

515-
pieces[i] = &piece{
509+
pieces = append(pieces, &piece{
516510
assetID: assetID,
517511
totalAvailable: sumByID,
518512
proofs: proofsByID,
519513
packet: pkt,
520-
}
514+
})
521515
}
522516

523517
// Make sure the pieces are in a stable and reproducible order before we
@@ -840,3 +834,22 @@ func setAllocationFieldsFromOutput(alloc *Allocation, vOut *tappsbt.VOutput) {
840834
alloc.AltLeaves = vOut.AltLeaves
841835
alloc.SiblingPreimage = vOut.AnchorOutputTapscriptSibling
842836
}
837+
838+
// GroupProofsByAssetID groups the given proofs by their asset ID.
839+
func GroupProofsByAssetID(proofs []*proof.Proof) map[asset.ID][]*proof.Proof {
840+
assetIDs := fn.Map(proofs, func(p *proof.Proof) asset.ID {
841+
return p.Asset.ID()
842+
})
843+
uniqueAssetIDs := fn.NewSet(assetIDs...).ToSlice()
844+
845+
groupedProofs := make(map[asset.ID][]*proof.Proof, len(uniqueAssetIDs))
846+
for _, assetID := range uniqueAssetIDs {
847+
groupedProofs[assetID] = fn.Filter(
848+
proofs, func(p *proof.Proof) bool {
849+
return p.Asset.ID() == assetID
850+
},
851+
)
852+
}
853+
854+
return groupedProofs
855+
}

0 commit comments

Comments
 (0)