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
0 commit comments