From 6fe078b6da908dda4c78783fdc216d016eafcfb6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:31 +0100 Subject: [PATCH 01/13] tapchannel+tapsend: move allocation code to tapsend package We'll generalize the allocation code in future commits so we can re-use it. We need to make sure we don't create circular package dependencies, so we move the code to a more appropriate location. --- tapchannel/aux_closer.go | 40 +++---- tapchannel/aux_sweeper.go | 29 ++--- tapchannel/commitment.go | 107 ++++++++++-------- {tapchannel => tapsend}/allocation.go | 15 ++- {tapchannel => tapsend}/allocation_sort.go | 2 +- .../allocation_sort_test.go | 2 +- {tapchannel => tapsend}/allocation_test.go | 2 +- 7 files changed, 104 insertions(+), 93 deletions(-) rename {tapchannel => tapsend}/allocation.go (97%) rename {tapchannel => tapsend}/allocation_sort.go (98%) rename {tapchannel => tapsend}/allocation_sort_test.go (99%) rename {tapchannel => tapsend}/allocation_test.go (99%) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index eaff1ece3..3794cd5c0 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -70,7 +70,7 @@ type assetCloseInfo struct { // allocations is the list of allocations for the remote+local party. // There'll be at most 4 of these: local+remote BTC outputs, // local+remote asset outputs. - allocations []*Allocation + allocations []*tapsend.Allocation // vPackets is the list of virtual packets that we'll use to anchor the // outputs. @@ -104,7 +104,7 @@ func NewAuxChanCloser(cfg AuxChanCloserCfg) *AuxChanCloser { // createCloseAlloc is a helper function that creates an allocation for an // asset close. func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, - shutdownMsg tapchannelmsg.AuxShutdownMsg) (*Allocation, error) { + shutdownMsg tapchannelmsg.AuxShutdownMsg) (*tapsend.Allocation, error) { assetID := closeAsset.ID() @@ -132,13 +132,13 @@ func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, "address: %w", err) } - return &Allocation{ - Type: func() AllocationType { + return &tapsend.Allocation{ + Type: func() tapsend.AllocationType { if isLocal { - return CommitAllocationToLocal + return tapsend.CommitAllocationToLocal } - return CommitAllocationToRemote + return tapsend.CommitAllocationToRemote }(), SplitRoot: isInitiator, InternalKey: shutdownMsg.AssetInternalKey.Val, @@ -256,8 +256,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( // outputs. We track the amount that'll go to the anchor assets, so we // can subtract this from the settled BTC amount. var ( - closeAllocs []*Allocation - localAlloc, remoteAlloc *Allocation + closeAllocs []*tapsend.Allocation + localAlloc, remoteAlloc *tapsend.Allocation localAssetAnchorAmt, remoteAssetAnchorAmt btcutil.Amount ) for _, localAssetProof := range commitState.LocalAssets.Val.Outputs { @@ -317,8 +317,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Snip off the first two bytes, as we'll be getting a P2TR // output from the higher level. We want a raw pubkey here. sortScript := o.PkScript[2:] - closeAllocs = append(closeAllocs, &Allocation{ - Type: AllocationTypeNoAssets, + closeAllocs = append(closeAllocs, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: amtAfterAnchor, SortTaprootKeyBytes: sortScript, InternalKey: localShutdown.BtcInternalKey.Val, @@ -347,8 +347,8 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Snip off the first two bytes, as we'll be getting a P2TR // output from the higher level. We want a raw pubkey here. sortScript := o.PkScript[2:] - closeAllocs = append(closeAllocs, &Allocation{ - Type: AllocationTypeNoAssets, + closeAllocs = append(closeAllocs, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: amtAfterAnchor, SortTaprootKeyBytes: sortScript, InternalKey: remoteShutdown.BtcInternalKey.Val, @@ -361,14 +361,14 @@ func (a *AuxChanCloser) AuxCloseOutputs( // With all allocations created, we now sort them to ensure that we // have a stable and deterministic order that both parties can arrive // at. We then assign the output indexes according to that order. - InPlaceAllocationSort(closeAllocs) + tapsend.InPlaceAllocationSort(closeAllocs) for idx := range closeAllocs { closeAllocs[idx].OutputIndex = uint32(idx) } // Now that we have the complete set of allocations, we'll distribute // them to create the vPackets we'll need to anchor everything. - vPackets, err := DistributeCoins( + vPackets, err := tapsend.DistributeCoins( inputProofs, closeAllocs, a.cfg.ChainParams, ) if err != nil { @@ -414,7 +414,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( return none, fmt.Errorf("unable to create output "+ "commitments: %w", err) } - err = AssignOutputCommitments(closeAllocs, outCommitments) + err = tapsend.AssignOutputCommitments(closeAllocs, outCommitments) if err != nil { return none, fmt.Errorf("unable to assign alloc output "+ "commitments: %w", err) @@ -432,11 +432,11 @@ func (a *AuxChanCloser) AuxCloseOutputs( // With the taproot keys updated, we know the pkScripts needed, so // we'll create the wallet option for the co-op close. var closeOutputs []lnwallet.CloseOutput - assetAllocations := fn.Filter(closeAllocs, FilterByTypeExclude( - AllocationTypeNoAssets, + assetAllocations := fn.Filter(closeAllocs, tapsend.FilterByTypeExclude( + tapsend.AllocationTypeNoAssets, )) for _, alloc := range assetAllocations { - pkScript, err := alloc.finalPkScript() + pkScript, err := alloc.FinalPkScript() if err != nil { return none, fmt.Errorf("unable to make final "+ "pkScript: %w", err) @@ -447,7 +447,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( PkScript: pkScript, Value: int64(alloc.BtcAmount), }, - IsLocal: alloc.Type == CommitAllocationToLocal, + IsLocal: alloc.Type == tapsend.CommitAllocationToLocal, }) } @@ -682,7 +682,7 @@ func (a *AuxChanCloser) FinalizeClose(desc chancloser.AuxCloseDesc, for idx := range closeInfo.vPackets { vPkt := closeInfo.vPackets[idx] for outIdx := range vPkt.Outputs { - exclusionCreator := NonAssetExclusionProofs( + exclusionCreator := tapsend.NonAssetExclusionProofs( closeInfo.allocations, ) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 7de9abb14..a9b628269 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -205,7 +205,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, return lfn.Err[returnType](err) } - allocs := make([]*Allocation, 0, len(sweepInputs)) + allocs := make([]*tapsend.Allocation, 0, len(sweepInputs)) ctx := context.Background() // If this is a second level HTLC sweep, then we already have @@ -250,8 +250,8 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // We leave out the internal key here, as we'll make it // later once we actually have the other set of inputs // we need to sweep. - allocs = append(allocs, &Allocation{ - Type: CommitAllocationToLocal, + allocs = append(allocs, &tapsend.Allocation{ + Type: tapsend.CommitAllocationToLocal, // We don't need to worry about sorting, as // we'll always be the first output index in the // transaction. @@ -280,7 +280,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // With the proofs constructed, we can now distribute the coins to // create the vPackets that we'll pass on to the next stage. - vPackets, err := DistributeCoins( + vPackets, err := tapsend.DistributeCoins( inputProofs, allocs, &a.cfg.ChainParams, ) if err != nil { @@ -1093,23 +1093,25 @@ func assetOutputToVPacket(fundingInputProofs map[asset.ID]*proof.Proof, // allocations for the anchor outputs. We'll use this later to create the proper // exclusion proofs. func anchorOutputAllocations( - keyRing *lnwallet.CommitmentKeyRing) lfn.Result[[]*Allocation] { + keyRing *lnwallet.CommitmentKeyRing) lfn.Result[[]*tapsend.Allocation] { + + anchorAlloc := func( + k *btcec.PublicKey) lfn.Result[*tapsend.Allocation] { - anchorAlloc := func(k *btcec.PublicKey) lfn.Result[*Allocation] { anchorTree, err := input.NewAnchorScriptTree(k) if err != nil { - return lfn.Err[*Allocation](err) + return lfn.Err[*tapsend.Allocation](err) } sibling, scriptTree, err := LeavesFromTapscriptScriptTree( anchorTree, ) if err != nil { - return lfn.Err[*Allocation](err) + return lfn.Err[*tapsend.Allocation](err) } - return lfn.Ok(&Allocation{ - Type: AllocationTypeNoAssets, + return lfn.Ok(&tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -1123,9 +1125,10 @@ func anchorOutputAllocations( localAnchor := anchorAlloc(keyRing.ToLocalKey) remoteAnchor := anchorAlloc(keyRing.ToRemoteKey) + type resultType = lfn.Result[[]*tapsend.Allocation] return lfn.AndThen2( localAnchor, remoteAnchor, - func(a1, a2 *Allocation) lfn.Result[[]*Allocation] { + func(a1, a2 *tapsend.Allocation) resultType { // Before we return the anchors, we'll make sure that // they end up in the right sort order. scriptCompare := bytes.Compare( @@ -1140,7 +1143,7 @@ func anchorOutputAllocations( a1.OutputIndex = 1 } - return lfn.Ok([]*Allocation{a1, a2}) + return lfn.Ok([]*tapsend.Allocation{a1, a2}) }, ) } @@ -1617,7 +1620,7 @@ func (a *AuxSweeper) importCommitTx(req lnwallet.ResolutionReq, return fmt.Errorf("unable to create anchor "+ "allocations: %w", err) } - exclusionCreator := NonAssetExclusionProofs(anchorAllocations) + exclusionCreator := tapsend.NonAssetExclusionProofs(anchorAllocations) for idx := range vPkts { vPkt := vPkts[idx] for outIdx := range vPkt.Outputs { diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 5dfd2932e..fc09c7c93 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -479,8 +479,8 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, whoseCommit lntypes.ChannelParty, ourBalance, theirBalance lnwire.MilliSatoshi, originalView *lnwallet.HtlcView, chainParams *address.ChainParams, - keys lnwallet.CommitmentKeyRing) ([]*Allocation, *cmsg.Commitment, - error) { + keys lnwallet.CommitmentKeyRing) ([]*tapsend.Allocation, + *cmsg.Commitment, error) { log.Tracef("Generating allocations, whoseCommit=%v, ourBalance=%d, "+ "theirBalance=%d", whoseCommit, ourBalance, theirBalance) @@ -555,7 +555,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // Now we can distribute the inputs according to the allocations. This // creates a virtual packet for each distinct asset ID that is committed // to the channel. - vPackets, err := DistributeCoins(inputProofs, allocations, chainParams) + vPackets, err := tapsend.DistributeCoins( + inputProofs, allocations, chainParams, + ) if err != nil { return nil, nil, fmt.Errorf("unable to distribute coins: %w", err) @@ -607,7 +609,7 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // The output commitment is all we need to create the auxiliary leaves. // We map the output commitments (which are keyed by on-chain output // index) back to the allocation. - err = AssignOutputCommitments(allocations, outCommitments) + err = tapsend.AssignOutputCommitments(allocations, outCommitments) if err != nil { return nil, nil, fmt.Errorf("unable to assign alloc output "+ "commitments: %w", err) @@ -631,7 +633,9 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, for outIdx := range vPkt.Outputs { proofSuffix, err := tapsend.CreateProofSuffixCustom( fakeCommitTx, vPkt, outCommitments, outIdx, - vPackets, NonAssetExclusionProofs(allocations), + vPackets, tapsend.NonAssetExclusionProofs( + allocations, + ), ) if err != nil { return nil, nil, fmt.Errorf("unable to create "+ @@ -661,7 +665,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, wantLocalCommitAnchor, wantRemoteCommitAnchor bool, filteredView *DecodedView, whoseCommit lntypes.ChannelParty, keys lnwallet.CommitmentKeyRing, - nonAssetView *DecodedView) ([]*Allocation, error) { + nonAssetView *DecodedView) ([]*tapsend.Allocation, error) { log.Tracef("Creating allocations, whoseCommit=%v, initiator=%v, "+ "ourBalance=%d, theirBalance=%d, ourAssetBalance=%d, "+ @@ -683,8 +687,8 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, len(filteredView.TheirUpdates) + len(nonAssetView.OurUpdates) + len(nonAssetView.TheirUpdates) + 4 - allocations = make([]*Allocation, 0, numAllocations) - addAlloc = func(a *Allocation) { + allocations = make([]*tapsend.Allocation, 0, numAllocations) + addAlloc = func(a *tapsend.Allocation) { allocations = append(allocations, a) } ) @@ -771,9 +775,9 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, haveHtlcSplitRoot = true } - allocType := CommitAllocationHtlcOutgoing + allocType := tapsend.CommitAllocationHtlcOutgoing if isIncoming { - allocType = CommitAllocationHtlcIncoming + allocType = tapsend.CommitAllocationHtlcIncoming } // If HTLC is dust, do not create allocation for it. @@ -803,7 +807,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, schnorr.SerializePubKey(htlcTree.TaprootKey), schnorr.SerializePubKey(tweakedTree.TaprootKey)) - allocations = append(allocations, &Allocation{ + allocations = append(allocations, &tapsend.Allocation{ Type: allocType, Amount: rfqmsg.Sum(htlc.AssetBalances), AssetVersion: asset.V1, @@ -882,8 +886,8 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, return nil } - allocations = append(allocations, &Allocation{ - Type: AllocationTypeNoAssets, + allocations = append(allocations, &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: htlc.Amount.ToSatoshis(), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, @@ -916,7 +920,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, // With all allocations created, we now sort them to ensure that we have // a stable and deterministic order that both parties can arrive at. We // then assign the output indexes according to that order. - InPlaceAllocationSort(allocations) + tapsend.InPlaceAllocationSort(allocations) for idx := range allocations { allocations[idx].OutputIndex = uint32(idx) } @@ -932,7 +936,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, theirBalance btcutil.Amount, ourAssetBalance, theirAssetBalance uint64, wantLocalCommitAnchor, wantRemoteCommitAnchor bool, keys lnwallet.CommitmentKeyRing, leaseExpiry uint32, - addAllocation func(a *Allocation)) error { + addAllocation func(a *tapsend.Allocation)) error { // Start with the commitment anchor outputs. localAnchor, remoteAnchor, err := lnwallet.CommitScriptAnchors( @@ -951,9 +955,9 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - addAllocation(&Allocation{ + addAllocation(&tapsend.Allocation{ // Commitment anchor outputs never carry assets. - Type: AllocationTypeNoAssets, + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -972,9 +976,9 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "script sibling: %w", err) } - addAllocation(&Allocation{ + addAllocation(&tapsend.Allocation{ // Commitment anchor outputs never carry assets. - Type: AllocationTypeNoAssets, + Type: tapsend.AllocationTypeNoAssets, Amount: 0, BtcAmount: lnwallet.AnchorSize, InternalKey: scriptTree.InternalKey, @@ -1006,8 +1010,8 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - allocation := &Allocation{ - Type: CommitAllocationToLocal, + allocation := &tapsend.Allocation{ + Type: tapsend.CommitAllocationToLocal, Amount: ourAssetBalance, AssetVersion: asset.V1, SplitRoot: initiator, @@ -1033,8 +1037,8 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, // If there are no assets, only BTC (for example due to a push // amount), the allocation looks simpler. if ourAssetBalance == 0 { - allocation = &Allocation{ - Type: AllocationTypeNoAssets, + allocation = &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: ourBalance, InternalKey: toLocalTree.InternalKey, NonAssetLeaves: sibling, @@ -1068,8 +1072,8 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } - allocation := &Allocation{ - Type: CommitAllocationToRemote, + allocation := &tapsend.Allocation{ + Type: tapsend.CommitAllocationToRemote, Amount: theirAssetBalance, AssetVersion: asset.V1, SplitRoot: !initiator, @@ -1096,8 +1100,8 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, // If there are no assets, only BTC (for example due to a push // amount), the allocation looks simpler. if theirAssetBalance == 0 { - allocation = &Allocation{ - Type: AllocationTypeNoAssets, + allocation = &tapsend.Allocation{ + Type: tapsend.AllocationTypeNoAssets, BtcAmount: theirBalance, InternalKey: toRemoteTree.InternalKey, NonAssetLeaves: sibling, @@ -1141,7 +1145,7 @@ func LeavesFromTapscriptScriptTree( } // ToCommitment converts the allocations to a Commitment struct. -func ToCommitment(allocations []*Allocation, +func ToCommitment(allocations []*tapsend.Allocation, vPackets []*tappsbt.VPacket) (*cmsg.Commitment, error) { var ( @@ -1154,7 +1158,9 @@ func ToCommitment(allocations []*Allocation, // Start with the to_local output. There should be at most one of these // outputs. - toLocal := fn.Filter(allocations, FilterByType(CommitAllocationToLocal)) + toLocal := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationToLocal, + )) switch { case len(toLocal) > 1: return nil, fmt.Errorf("expected at most one to local output, "+ @@ -1176,9 +1182,9 @@ func ToCommitment(allocations []*Allocation, } // The same for the to_remote, at most one should exist. - toRemote := fn.Filter( - allocations, FilterByType(CommitAllocationToRemote), - ) + toRemote := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationToRemote, + )) switch { case len(toRemote) > 1: return nil, fmt.Errorf("expected at most one to remote "+ @@ -1199,9 +1205,9 @@ func ToCommitment(allocations []*Allocation, } } - outgoing := fn.Filter( - allocations, FilterByType(CommitAllocationHtlcOutgoing), - ) + outgoing := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationHtlcOutgoing, + )) for _, a := range outgoing { htlcLeaf, err := a.AuxLeaf() if err != nil { @@ -1230,9 +1236,9 @@ func ToCommitment(allocations []*Allocation, } } - incoming := fn.Filter( - allocations, FilterByType(CommitAllocationHtlcIncoming), - ) + incoming := fn.Filter(allocations, tapsend.FilterByType( + tapsend.CommitAllocationHtlcIncoming, + )) for _, a := range incoming { htlcLeaf, err := a.AuxLeaf() if err != nil { @@ -1269,7 +1275,7 @@ func ToCommitment(allocations []*Allocation, // collectOutputs collects all virtual transaction outputs for a given // allocation from the given packets. -func collectOutputs(a *Allocation, +func collectOutputs(a *tapsend.Allocation, allPackets []*tappsbt.VPacket) ([]*cmsg.AssetOutput, error) { var outputs []*cmsg.AssetOutput @@ -1303,7 +1309,7 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, initiator bool, htlcOutputs []*cmsg.AssetOutput, htlcAmt btcutil.Amount, commitCsvDelay uint32, keys lnwallet.CommitmentKeyRing, outputIndex fn.Option[uint32], htlcTimeout fn.Option[uint32], - htlcIndex uint64) ([]*Allocation, error) { + htlcIndex uint64) ([]*tapsend.Allocation, error) { // TODO(roasbeef): thaw height not implemented for taproot chans rn // (lease expiry) @@ -1338,8 +1344,8 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, schnorr.SerializePubKey(htlcTree.TaprootKey), schnorr.SerializePubKey(tweakedTree.TaprootKey)) - allocations := []*Allocation{{ - Type: SecondLevelHtlcAllocation, + allocations := []*tapsend.Allocation{{ + Type: tapsend.SecondLevelHtlcAllocation, // If we're making the second-level transaction just to sign, // then we'll have an output index of zero. Otherwise, we'll // want to use the output index as appears in the final @@ -1372,7 +1378,7 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, commitTx *wire.MsgTx, htlcAmt btcutil.Amount, keys lnwallet.CommitmentKeyRing, chainParams *address.ChainParams, htlcOutputs []*cmsg.AssetOutput, htlcTimeout fn.Option[uint32], - htlcIndex uint64) ([]*tappsbt.VPacket, []*Allocation, error) { + htlcIndex uint64) ([]*tappsbt.VPacket, []*tapsend.Allocation, error) { allocations, err := createSecondLevelHtlcAllocations( chanState.ChanType, chanState.IsInitiator, @@ -1391,7 +1397,9 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, }, ) - vPackets, err := DistributeCoins(inputProofs, allocations, chainParams) + vPackets, err := tapsend.DistributeCoins( + inputProofs, allocations, chainParams, + ) if err != nil { return nil, nil, fmt.Errorf("error distributing coins: %w", err) } @@ -1445,7 +1453,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, // The output commitment is all we need to create the auxiliary leaves. // We map the output commitments (which are keyed by on-chain output // index) back to the allocation. - err = AssignOutputCommitments(allocations, outCommitments) + err = tapsend.AssignOutputCommitments(allocations, outCommitments) if err != nil { return none, fmt.Errorf("unable to assign output commitments: "+ "%w", err) @@ -1464,7 +1472,7 @@ func CreateSecondLevelHtlcTx(chanState lnwallet.AuxChanState, // FakeCommitTx creates a fake commitment on-chain transaction from the given // funding outpoint and allocations. The transaction is not signed. func FakeCommitTx(fundingOutpoint wire.OutPoint, - allocations []*Allocation) (*wire.MsgTx, error) { + allocations []*tapsend.Allocation) (*wire.MsgTx, error) { fakeCommitTx := wire.NewMsgTx(2) fakeCommitTx.TxIn = []*wire.TxIn{ @@ -1475,7 +1483,7 @@ func FakeCommitTx(fundingOutpoint wire.OutPoint, fakeCommitTx.TxOut = make([]*wire.TxOut, len(allocations)) for _, a := range allocations { - pkScript, err := a.finalPkScript() + pkScript, err := a.FinalPkScript() if err != nil { return nil, fmt.Errorf("error getting final pk "+ "script: %w", err) @@ -1495,7 +1503,8 @@ func FakeCommitTx(fundingOutpoint wire.OutPoint, // the allocation's OutputIndex. The transaction inputs are sorted by the // default BIP69 sort. func InPlaceCustomCommitSort(tx *wire.MsgTx, cltvs []uint32, - htlcIndexes []input.HtlcIndex, allocations []*Allocation) error { + htlcIndexes []input.HtlcIndex, + allocations []*tapsend.Allocation) error { if len(tx.TxOut) != len(allocations) { return fmt.Errorf("output and allocation size mismatch") @@ -1515,7 +1524,7 @@ func InPlaceCustomCommitSort(tx *wire.MsgTx, cltvs []uint32, newCltvs := make([]uint32, len(cltvs)) for i, original := range txOutOriginal { - var allocation *Allocation + var allocation *tapsend.Allocation for _, a := range allocations { match, err := a.MatchesOutput( original.PkScript, original.Value, cltvs[i], diff --git a/tapchannel/allocation.go b/tapsend/allocation.go similarity index 97% rename from tapchannel/allocation.go rename to tapsend/allocation.go index 35450d4f1..c44462a70 100644 --- a/tapchannel/allocation.go +++ b/tapsend/allocation.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "bytes" @@ -17,7 +17,6 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tappsbt" - "github.com/lightninglabs/taproot-assets/tapsend" "github.com/lightningnetwork/lnd/input" ) @@ -172,10 +171,10 @@ func (a *Allocation) tapscriptSibling() (*commitment.TapscriptPreimage, error) { return sibling, err } -// finalPkScript returns the pkScript calculated from the internal key, +// FinalPkScript returns the pkScript calculated from the internal key, // tapscript sibling and merkle root of the output commitment. If the output // commitment is not set, ErrCommitmentNotSet is returned. -func (a *Allocation) finalPkScript() ([]byte, error) { +func (a *Allocation) FinalPkScript() ([]byte, error) { // If this is a normal commitment anchor output without any assets, then // we'll map the sort Taproot output key to a script directly. if a.Type == AllocationTypeNoAssets { @@ -229,7 +228,7 @@ func (a *Allocation) AuxLeaf() (txscript.TapLeaf, error) { func (a *Allocation) MatchesOutput(pkScript []byte, value int64, cltv uint32, htlcIndex input.HtlcIndex) (bool, error) { - finalPkScript, err := a.finalPkScript() + finalPkScript, err := a.FinalPkScript() if err != nil { return false, err } @@ -478,7 +477,7 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, packets := fn.Map(pieces, func(p *piece) *tappsbt.VPacket { return p.packet }) - err := tapsend.ValidateVPacketVersions(packets) + err := ValidateVPacketVersions(packets) if err != nil { return nil, err } @@ -515,10 +514,10 @@ func AssignOutputCommitments(allocations []*Allocation, // NonAssetExclusionProofs returns an exclusion proof generator that creates // exclusion proofs for non-asset P2TR outputs in the given allocations. func NonAssetExclusionProofs( - allocations []*Allocation) tapsend.ExclusionProofGenerator { + allocations []*Allocation) ExclusionProofGenerator { return func(target *proof.BaseProofParams, - isAnchor tapsend.IsAnchor) error { + isAnchor IsAnchor) error { for _, alloc := range allocations { // We only need exclusion proofs for allocations that diff --git a/tapchannel/allocation_sort.go b/tapsend/allocation_sort.go similarity index 98% rename from tapchannel/allocation_sort.go rename to tapsend/allocation_sort.go index 12be25943..634b5d0ef 100644 --- a/tapchannel/allocation_sort.go +++ b/tapsend/allocation_sort.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "bytes" diff --git a/tapchannel/allocation_sort_test.go b/tapsend/allocation_sort_test.go similarity index 99% rename from tapchannel/allocation_sort_test.go rename to tapsend/allocation_sort_test.go index a2f385992..bfba13490 100644 --- a/tapchannel/allocation_sort_test.go +++ b/tapsend/allocation_sort_test.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "testing" diff --git a/tapchannel/allocation_test.go b/tapsend/allocation_test.go similarity index 99% rename from tapchannel/allocation_test.go rename to tapsend/allocation_test.go index 8ca318467..7ad982ff2 100644 --- a/tapchannel/allocation_test.go +++ b/tapsend/allocation_test.go @@ -1,4 +1,4 @@ -package tapchannel +package tapsend import ( "testing" From 543d8cca77bb54789184d6f7913f29905052f09b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:32 +0100 Subject: [PATCH 02/13] tapsend: remove already fixed TODO, clarify usage --- tapsend/allocation.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tapsend/allocation.go b/tapsend/allocation.go index c44462a70..622cec299 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -133,7 +133,8 @@ type Allocation struct { CLTV uint32 // Sequence is the CSV value for the asset allocation. This is only - // relevant for HTLC second level transactions. + // relevant for HTLC second level transactions. This value will be set + // as the relative time lock on the virtual output. Sequence uint32 // HtlcIndex is the index of the HTLC that the allocation is for. This @@ -458,9 +459,6 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, } p.packet.Outputs = append(p.packet.Outputs, vOut) - // TODO(guggero): If sequence > 0, set the sequence - // on the inputs of the packet. - p.allocated += allocating toFill -= allocating From 06ee39fe70d49f77d13bc2129debc7c47709ad33 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:34 +0100 Subject: [PATCH 03/13] multi: make vPacket version configurable The version of the virtual packet has an influence on the commitment versions of the outputs created from it, so it's important that it can be configured in a more generic use of the allocation code. --- tapchannel/aux_closer.go | 2 +- tapchannel/aux_sweeper.go | 3 ++- tapchannel/commitment.go | 4 ++-- tappsbt/create.go | 6 +++--- tapsend/allocation.go | 7 +++++-- tapsend/allocation_test.go | 16 +++++++++++++--- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index 3794cd5c0..33933bd18 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -369,7 +369,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Now that we have the complete set of allocations, we'll distribute // them to create the vPackets we'll need to anchor everything. vPackets, err := tapsend.DistributeCoins( - inputProofs, closeAllocs, a.cfg.ChainParams, + inputProofs, closeAllocs, a.cfg.ChainParams, tappsbt.V1, ) if err != nil { return none, fmt.Errorf("unable to distribute coins: %w", err) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index a9b628269..0febb9bf5 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -281,7 +281,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // With the proofs constructed, we can now distribute the coins to // create the vPackets that we'll pass on to the next stage. vPackets, err := tapsend.DistributeCoins( - inputProofs, allocs, &a.cfg.ChainParams, + inputProofs, allocs, &a.cfg.ChainParams, tappsbt.V1, ) if err != nil { return lfn.Errf[returnType]("error distributing coins: %w", err) @@ -1036,6 +1036,7 @@ func assetOutputToVPacket(fundingInputProofs map[asset.ID]*proof.Proof, } vPkt, err = tappsbt.FromProofs( []*proof.Proof{fundingInputProof}, chainParams, + tappsbt.V1, ) if err != nil { return fmt.Errorf("unable to create "+ diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index fc09c7c93..fc9ce7cd4 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -556,7 +556,7 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // creates a virtual packet for each distinct asset ID that is committed // to the channel. vPackets, err := tapsend.DistributeCoins( - inputProofs, allocations, chainParams, + inputProofs, allocations, chainParams, tappsbt.V1, ) if err != nil { return nil, nil, fmt.Errorf("unable to distribute coins: %w", @@ -1398,7 +1398,7 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, ) vPackets, err := tapsend.DistributeCoins( - inputProofs, allocations, chainParams, + inputProofs, allocations, chainParams, tappsbt.V1, ) if err != nil { return nil, nil, fmt.Errorf("error distributing coins: %w", err) diff --git a/tappsbt/create.go b/tappsbt/create.go index cbf510aa5..5b1160db9 100644 --- a/tappsbt/create.go +++ b/tappsbt/create.go @@ -171,12 +171,12 @@ func OwnershipProofPacket(ownedAsset *asset.Asset, // FromProofs creates a packet from the given proofs that adds them as inputs to // the packet. -func FromProofs(proofs []*proof.Proof, - params *address.ChainParams) (*VPacket, error) { +func FromProofs(proofs []*proof.Proof, params *address.ChainParams, + version VPacketVersion) (*VPacket, error) { pkt := &VPacket{ ChainParams: params, - Version: V1, + Version: version, } for idx := range proofs { diff --git a/tapsend/allocation.go b/tapsend/allocation.go index 622cec299..cffb27d9e 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -328,7 +328,8 @@ func sortPiecesWithProofs(pieces []*piece) { // asset outputs (asset UTXOs of different sizes from different tranches/asset // IDs) according to the distribution rules provided as "allocations". func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, - chainParams *address.ChainParams) ([]*tappsbt.VPacket, error) { + chainParams *address.ChainParams, + vPktVersion tappsbt.VPacketVersion) ([]*tappsbt.VPacket, error) { if len(inputs) == 0 { return nil, ErrMissingInputs @@ -383,7 +384,9 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, }, ) - pkt, err := tappsbt.FromProofs(proofsByID, chainParams) + pkt, err := tappsbt.FromProofs( + proofsByID, chainParams, vPktVersion, + ) if err != nil { return nil, err } diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go index 7ad982ff2..74e03e2ac 100644 --- a/tapsend/allocation_test.go +++ b/tapsend/allocation_test.go @@ -61,16 +61,19 @@ func grindAssetID(t *testing.T, prefix byte) asset.Genesis { } func TestDistributeCoinsErrors(t *testing.T) { - _, err := DistributeCoins(nil, nil, testParams) + _, err := DistributeCoins(nil, nil, testParams, tappsbt.V1) require.ErrorIs(t, err, ErrMissingInputs) - _, err = DistributeCoins([]*proof.Proof{{}}, nil, testParams) + _, err = DistributeCoins( + []*proof.Proof{{}}, nil, testParams, tappsbt.V1, + ) require.ErrorIs(t, err, ErrMissingAllocations) assetCollectible := asset.RandAsset(t, asset.Collectible) proofCollectible := makeProof(t, assetCollectible) _, err = DistributeCoins( []*proof.Proof{proofCollectible}, []*Allocation{{}}, testParams, + tappsbt.V1, ) require.ErrorIs(t, err, ErrNormalAssetsOnly) @@ -81,7 +84,7 @@ func TestDistributeCoinsErrors(t *testing.T) { { Amount: assetNormal.Amount / 2, }, - }, testParams, + }, testParams, tappsbt.V1, ) require.ErrorIs(t, err, ErrInputOutputSumMismatch) } @@ -142,6 +145,7 @@ func TestDistributeCoins(t *testing.T) { name string inputs []*proof.Proof allocations []*Allocation + vPktVersion tappsbt.VPacketVersion expectedInputs map[asset.ID][]asset.ScriptKey expectedOutputs map[asset.ID][]*tappsbt.VOutput }{ @@ -162,6 +166,7 @@ func TestDistributeCoins(t *testing.T) { OutputIndex: 1, }, }, + vPktVersion: tappsbt.V1, expectedInputs: map[asset.ID][]asset.ScriptKey{ assetID1.ID(): { assetID1Tranche1.ScriptKey, @@ -202,6 +207,7 @@ func TestDistributeCoins(t *testing.T) { OutputIndex: 1, }, }, + vPktVersion: tappsbt.V1, expectedInputs: map[asset.ID][]asset.ScriptKey{ assetID2.ID(): { assetID2Tranche1.ScriptKey, @@ -357,6 +363,7 @@ func TestDistributeCoins(t *testing.T) { t.Run(tc.name, func(t *testing.T) { packets, err := DistributeCoins( tc.inputs, tc.allocations, testParams, + tc.vPktVersion, ) require.NoError(t, err) @@ -364,6 +371,9 @@ func TestDistributeCoins(t *testing.T) { t, packets, tc.expectedInputs, tc.expectedOutputs, ) + for _, pkt := range packets { + require.Equal(t, tc.vPktVersion, pkt.Version) + } }) } } From 11f05dabc56a3dbdab8d1968f3794db30ade33b3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:35 +0100 Subject: [PATCH 04/13] tapsend: refactor vOutput creation This is a preparatory commit that refactors the piece allocation code a bit. This will make the actual changes in the next commit more easy to understand. This commit changes nothing in the behavior of the code. --- tapsend/allocation.go | 106 ++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/tapsend/allocation.go b/tapsend/allocation.go index cffb27d9e..f6d6f431e 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -415,55 +415,15 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, // Find the next piece that has assets left to allocate. toFill := a.Amount for pieceIdx := range pieces { - p := pieces[pieceIdx] - - // Skip fully allocated pieces - if p.available() == 0 { - continue - } - - // We know we have something to allocate, so let's now - // create a new vOutput for the allocation. - allocating := toFill - if p.available() < toFill { - allocating = p.available() - } - - // We only need a split root output if this piece is - // being split. If we consume it fully in this - // allocation, we can use a simple output. - consumeFully := p.allocated == 0 && - toFill >= p.available() - - outType := tappsbt.TypeSimple - if a.SplitRoot && !consumeFully { - outType = tappsbt.TypeSplitRoot - } - - sibling, err := a.tapscriptSibling() + fillDelta, updatedPiece, err := allocatePiece( + *pieces[pieceIdx], *a, toFill, + ) if err != nil { return nil, err } - deliveryAddr := a.ProofDeliveryAddress - vOut := &tappsbt.VOutput{ - Amount: allocating, - AssetVersion: a.AssetVersion, - Type: outType, - Interactive: true, - AnchorOutputIndex: a.OutputIndex, - AnchorOutputInternalKey: a.InternalKey, - AnchorOutputTapscriptSibling: sibling, - ScriptKey: a.ScriptKey, - ProofDeliveryAddress: deliveryAddr, - RelativeLockTime: uint64( - a.Sequence, - ), - } - p.packet.Outputs = append(p.packet.Outputs, vOut) - - p.allocated += allocating - toFill -= allocating + pieces[pieceIdx] = updatedPiece + toFill -= fillDelta // If the piece has enough assets to fill the // allocation, we can exit the loop. If it only fills @@ -486,6 +446,62 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, return packets, nil } +// allocatePiece allocates assets from the given piece to the given allocation, +// if there are units left to allocate. This adds a virtual output to the piece +// and updates the amount of allocated assets. The function returns the amount +// of assets that were allocated and the updated piece. +func allocatePiece(p piece, a Allocation, + toFill uint64) (uint64, *piece, error) { + + sibling, err := a.tapscriptSibling() + if err != nil { + return 0, nil, err + } + + deliveryAddr := a.ProofDeliveryAddress + vOut := &tappsbt.VOutput{ + AssetVersion: a.AssetVersion, + Interactive: true, + AnchorOutputIndex: a.OutputIndex, + AnchorOutputInternalKey: a.InternalKey, + AnchorOutputTapscriptSibling: sibling, + ScriptKey: a.ScriptKey, + ProofDeliveryAddress: deliveryAddr, + RelativeLockTime: uint64(a.Sequence), + } + + // Skip fully allocated pieces. + if p.available() == 0 { + return 0, &p, nil + } + + // We know we have something to allocate, so let's now create a new + // vOutput for the allocation. + allocating := toFill + if p.available() < toFill { + allocating = p.available() + } + + // We only need a split root output if this piece is being split. If we + // consume it fully in this allocation, we can use a simple output. + consumeFully := p.allocated == 0 && toFill >= p.available() + + outType := tappsbt.TypeSimple + if a.SplitRoot && !consumeFully { + outType = tappsbt.TypeSplitRoot + } + + // We just need to update the type and amount for this virtual output, + // everything else can be taken from the allocation itself. + vOut.Type = outType + vOut.Amount = allocating + p.packet.Outputs = append(p.packet.Outputs, vOut) + + p.allocated += allocating + + return allocating, &p, nil +} + // AssignOutputCommitments assigns the output commitments keyed by the output // index to the corresponding allocations. func AssignOutputCommitments(allocations []*Allocation, From c4f5cf72cc5ca02a0e9d7bd6c5c30429898c2c25 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:36 +0100 Subject: [PATCH 05/13] tapchannel+tapsend: make allocation compatible with non-interactive The allocation code was previously only used for channel outputs, where all virtual transactions are always interactive. But we want to use the same code for non-interactive transfers (address based send logic) as well, which requires a couple of tweaks. --- tapchannel/aux_closer.go | 2 +- tapchannel/aux_sweeper.go | 2 +- tapchannel/commitment.go | 4 +- tappsbt/create.go | 1 + tapsend/allocation.go | 77 ++++-- tapsend/allocation_test.go | 513 ++++++++++++++++++++++++++++++++++--- 6 files changed, 545 insertions(+), 54 deletions(-) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index 33933bd18..589827da2 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -369,7 +369,7 @@ func (a *AuxChanCloser) AuxCloseOutputs( // Now that we have the complete set of allocations, we'll distribute // them to create the vPackets we'll need to anchor everything. vPackets, err := tapsend.DistributeCoins( - inputProofs, closeAllocs, a.cfg.ChainParams, tappsbt.V1, + inputProofs, closeAllocs, a.cfg.ChainParams, true, tappsbt.V1, ) if err != nil { return none, fmt.Errorf("unable to distribute coins: %w", err) diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index 0febb9bf5..b48e18f57 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -281,7 +281,7 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // With the proofs constructed, we can now distribute the coins to // create the vPackets that we'll pass on to the next stage. vPackets, err := tapsend.DistributeCoins( - inputProofs, allocs, &a.cfg.ChainParams, tappsbt.V1, + inputProofs, allocs, &a.cfg.ChainParams, true, tappsbt.V1, ) if err != nil { return lfn.Errf[returnType]("error distributing coins: %w", err) diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index fc9ce7cd4..33faa8a27 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -556,7 +556,7 @@ func GenerateCommitmentAllocations(prevState *cmsg.Commitment, // creates a virtual packet for each distinct asset ID that is committed // to the channel. vPackets, err := tapsend.DistributeCoins( - inputProofs, allocations, chainParams, tappsbt.V1, + inputProofs, allocations, chainParams, true, tappsbt.V1, ) if err != nil { return nil, nil, fmt.Errorf("unable to distribute coins: %w", @@ -1398,7 +1398,7 @@ func CreateSecondLevelHtlcPackets(chanState lnwallet.AuxChanState, ) vPackets, err := tapsend.DistributeCoins( - inputProofs, allocations, chainParams, tappsbt.V1, + inputProofs, allocations, chainParams, true, tappsbt.V1, ) if err != nil { return nil, nil, fmt.Errorf("error distributing coins: %w", err) diff --git a/tappsbt/create.go b/tappsbt/create.go index 5b1160db9..5e262144b 100644 --- a/tappsbt/create.go +++ b/tappsbt/create.go @@ -54,6 +54,7 @@ func FromAddresses(receiverAddrs []*address.Tap, // process. pkt.Outputs = append(pkt.Outputs, &VOutput{ Amount: 0, + Interactive: false, Type: TypeSplitRoot, AnchorOutputIndex: 0, ScriptKey: asset.NUMSScriptKey, diff --git a/tapsend/allocation.go b/tapsend/allocation.go index f6d6f431e..3fdddf5aa 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -29,15 +29,19 @@ var ( // were provided. ErrMissingAllocations = fmt.Errorf("no allocations provided") + // ErrInputTypesNotEqual is an error that is returned if the input types + // are not all the same. + ErrInputTypesNotEqual = fmt.Errorf("input types not all equal") + + // ErrInputGroupMismatch is an error that is returned if the input + // assets don't all belong to the same asset group. + ErrInputGroupMismatch = fmt.Errorf("input assets not all of same group") + // ErrInputOutputSumMismatch is an error that is returned if the sum of // the input asset proofs does not match the sum of the output // allocations. ErrInputOutputSumMismatch = fmt.Errorf("input and output sum mismatch") - // ErrNormalAssetsOnly is an error that is returned if an allocation - // contains an asset that is not a normal asset (e.g. a collectible). - ErrNormalAssetsOnly = fmt.Errorf("only normal assets are supported") - // ErrCommitmentNotSet is an error that is returned if the output // commitment is not set for an allocation. ErrCommitmentNotSet = fmt.Errorf("output commitment not set") @@ -328,7 +332,7 @@ func sortPiecesWithProofs(pieces []*piece) { // asset outputs (asset UTXOs of different sizes from different tranches/asset // IDs) according to the distribution rules provided as "allocations". func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, - chainParams *address.ChainParams, + chainParams *address.ChainParams, interactive bool, vPktVersion tappsbt.VPacketVersion) ([]*tappsbt.VPacket, error) { if len(inputs) == 0 { @@ -340,10 +344,21 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, } // Count how many asset units are available for distribution. - var inputSum uint64 + var ( + inputSum uint64 + firstType = inputs[0].Asset.Type + firstTapKey = inputs[0].Asset.TapCommitmentKey() + ) for _, inputProof := range inputs { - if inputProof.Asset.Type != asset.Normal { - return nil, ErrNormalAssetsOnly + // We can't have mixed types (normal and collectibles) within + // the same allocation. + if firstType != inputProof.Asset.Type { + return nil, ErrInputTypesNotEqual + } + + // Allocating assets from different asset groups is not allowed. + if firstTapKey != inputProof.Asset.TapCommitmentKey() { + return nil, ErrInputGroupMismatch } inputSum += inputProof.Asset.Amount @@ -416,7 +431,7 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, toFill := a.Amount for pieceIdx := range pieces { fillDelta, updatedPiece, err := allocatePiece( - *pieces[pieceIdx], *a, toFill, + *pieces[pieceIdx], *a, toFill, interactive, ) if err != nil { return nil, err @@ -426,10 +441,11 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, toFill -= fillDelta // If the piece has enough assets to fill the - // allocation, we can exit the loop. If it only fills - // part of the allocation, we'll continue to the next - // piece. - if toFill == 0 { + // allocation, we can exit the loop, unless we also need + // to create a tombstone output for a non-interactive + // send. If it only fills part of the allocation, we'll + // continue to the next piece. + if toFill == 0 && interactive { break } } @@ -450,8 +466,8 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, // if there are units left to allocate. This adds a virtual output to the piece // and updates the amount of allocated assets. The function returns the amount // of assets that were allocated and the updated piece. -func allocatePiece(p piece, a Allocation, - toFill uint64) (uint64, *piece, error) { +func allocatePiece(p piece, a Allocation, toFill uint64, + interactive bool) (uint64, *piece, error) { sibling, err := a.tapscriptSibling() if err != nil { @@ -461,7 +477,7 @@ func allocatePiece(p piece, a Allocation, deliveryAddr := a.ProofDeliveryAddress vOut := &tappsbt.VOutput{ AssetVersion: a.AssetVersion, - Interactive: true, + Interactive: interactive, AnchorOutputIndex: a.OutputIndex, AnchorOutputInternalKey: a.InternalKey, AnchorOutputTapscriptSibling: sibling, @@ -470,8 +486,21 @@ func allocatePiece(p piece, a Allocation, RelativeLockTime: uint64(a.Sequence), } - // Skip fully allocated pieces. - if p.available() == 0 { + // If we've allocated all pieces, or we don't need to allocate anything + // to this piece, we might only need to create a tombstone output. + if p.available() == 0 || toFill == 0 { + // We don't need a tombstone output for interactive transfers, + // or recipient outputs (outputs that don't go back to the + // sender). + if interactive || !a.SplitRoot { + return 0, &p, nil + } + + // Create a zero-amount tombstone output for the split root, if + // there is no change. + vOut.Type = tappsbt.TypeSplitRoot + p.packet.Outputs = append(p.packet.Outputs, vOut) + return 0, &p, nil } @@ -486,8 +515,18 @@ func allocatePiece(p piece, a Allocation, // consume it fully in this allocation, we can use a simple output. consumeFully := p.allocated == 0 && toFill >= p.available() + // If we're creating a non-interactive packet (e.g. for a TAP address + // based send), we definitely need a split root, even if there is no + // change. If there is change, then we also need a split root, even if + // we're creating a fully interactive packet. + needSplitRoot := a.SplitRoot && (!interactive || !consumeFully) + + // The only exception is when the split root output is the only output, + // because it's not being used at all and goes back to the sender. + splitRootIsOnlyOutput := a.SplitRoot && consumeFully + outType := tappsbt.TypeSimple - if a.SplitRoot && !consumeFully { + if needSplitRoot && !splitRootIsOnlyOutput { outType = tappsbt.TypeSplitRoot } diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go index 74e03e2ac..b18ff0ba8 100644 --- a/tapsend/allocation_test.go +++ b/tapsend/allocation_test.go @@ -61,30 +61,38 @@ func grindAssetID(t *testing.T, prefix byte) asset.Genesis { } func TestDistributeCoinsErrors(t *testing.T) { - _, err := DistributeCoins(nil, nil, testParams, tappsbt.V1) + _, err := DistributeCoins(nil, nil, testParams, true, tappsbt.V1) require.ErrorIs(t, err, ErrMissingInputs) _, err = DistributeCoins( - []*proof.Proof{{}}, nil, testParams, tappsbt.V1, + []*proof.Proof{{}}, nil, testParams, true, tappsbt.V1, ) require.ErrorIs(t, err, ErrMissingAllocations) + assetNormal := asset.RandAsset(t, asset.Normal) + proofNormal := makeProof(t, assetNormal) assetCollectible := asset.RandAsset(t, asset.Collectible) proofCollectible := makeProof(t, assetCollectible) _, err = DistributeCoins( - []*proof.Proof{proofCollectible}, []*Allocation{{}}, testParams, - tappsbt.V1, + []*proof.Proof{proofNormal, proofCollectible}, + []*Allocation{{}}, testParams, true, tappsbt.V1, ) - require.ErrorIs(t, err, ErrNormalAssetsOnly) + require.ErrorIs(t, err, ErrInputTypesNotEqual) + + assetNormal2 := asset.RandAsset(t, asset.Normal) + proofNormal2 := makeProof(t, assetNormal2) + _, err = DistributeCoins( + []*proof.Proof{proofNormal, proofNormal2}, + []*Allocation{{}}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInputGroupMismatch) - assetNormal := asset.RandAsset(t, asset.Normal) - proofNormal := makeProof(t, assetNormal) _, err = DistributeCoins( []*proof.Proof{proofNormal}, []*Allocation{ { Amount: assetNormal.Amount / 2, }, - }, testParams, tappsbt.V1, + }, testParams, true, tappsbt.V1, ) require.ErrorIs(t, err, ErrInputOutputSumMismatch) } @@ -92,49 +100,42 @@ func TestDistributeCoinsErrors(t *testing.T) { func TestDistributeCoins(t *testing.T) { t.Parallel() - assetID1 := grindAssetID(t, 0x01) - groupKey1 := &asset.GroupKey{ + groupKey := &asset.GroupKey{ GroupPubKey: *test.RandPubKey(t), } + assetID1 := grindAssetID(t, 0x01) assetID2 := grindAssetID(t, 0x02) - groupKey2 := &asset.GroupKey{ - GroupPubKey: *test.RandPubKey(t), - } - assetID3 := grindAssetID(t, 0x03) - groupKey3 := &asset.GroupKey{ - GroupPubKey: *test.RandPubKey(t), - } assetID1Tranche1 := asset.NewAssetNoErr( - t, assetID1, 100, 0, 0, asset.RandScriptKey(t), groupKey1, + t, assetID1, 100, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID1Tranche2 := asset.NewAssetNoErr( - t, assetID1, 200, 0, 0, asset.RandScriptKey(t), groupKey1, + t, assetID1, 200, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID1Tranche3 := asset.NewAssetNoErr( - t, assetID1, 300, 0, 0, asset.RandScriptKey(t), groupKey1, + t, assetID1, 300, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID2Tranche1 := asset.NewAssetNoErr( - t, assetID2, 1000, 0, 0, asset.RandScriptKey(t), groupKey2, + t, assetID2, 1000, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID2Tranche2 := asset.NewAssetNoErr( - t, assetID2, 2000, 0, 0, asset.RandScriptKey(t), groupKey2, + t, assetID2, 2000, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID2Tranche3 := asset.NewAssetNoErr( - t, assetID2, 3000, 0, 0, asset.RandScriptKey(t), groupKey2, + t, assetID2, 3000, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID3Tranche1 := asset.NewAssetNoErr( - t, assetID3, 10000, 0, 0, asset.RandScriptKey(t), groupKey3, + t, assetID3, 10000, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID3Tranche2 := asset.NewAssetNoErr( - t, assetID3, 20000, 0, 0, asset.RandScriptKey(t), groupKey3, + t, assetID3, 20000, 0, 0, asset.RandScriptKey(t), groupKey, ) assetID3Tranche3 := asset.NewAssetNoErr( - t, assetID3, 30000, 0, 0, asset.RandScriptKey(t), groupKey3, + t, assetID3, 30000, 0, 0, asset.RandScriptKey(t), groupKey, ) var ( @@ -144,16 +145,18 @@ func TestDistributeCoins(t *testing.T) { testCases := []struct { name string inputs []*proof.Proof + interactive bool allocations []*Allocation vPktVersion tappsbt.VPacketVersion expectedInputs map[asset.ID][]asset.ScriptKey expectedOutputs map[asset.ID][]*tappsbt.VOutput }{ { - name: "single asset, split", + name: "single asset, split, interactive", inputs: []*proof.Proof{ makeProof(t, assetID1Tranche1), }, + interactive: true, allocations: []*Allocation{ { Type: CommitAllocationToLocal, @@ -190,11 +193,161 @@ func TestDistributeCoins(t *testing.T) { }, }, { - name: "multiple assets, split", + name: "single asset, split, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 50, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 50, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 50, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: split, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "single asset, full value, interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 100, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 0, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: true, + AnchorOutputIndex: 0, + }, + }, + }, + }, + { + name: "single asset, full value, interactive, has " + + "split output", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 0, + SplitRoot: true, + }, + { + Type: CommitAllocationToRemote, + Amount: 100, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: true, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "single asset, full value, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + Amount: 100, + }, + { + Type: CommitAllocationToRemote, + SplitRoot: true, + Amount: 0, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 100, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, split, interactive", inputs: []*proof.Proof{ makeProof(t, assetID2Tranche1), makeProof(t, assetID2Tranche2), }, + interactive: true, allocations: []*Allocation{ { Type: CommitAllocationToLocal, @@ -232,11 +385,55 @@ func TestDistributeCoins(t *testing.T) { }, }, { - name: "multiple assets, one consumed fully", + name: "multiple assets, split, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 1200, + }, + { + Type: CommitAllocationToRemote, + Amount: 1800, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID2.ID(): { + { + Amount: 1200, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 1800, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "multiple assets, one consumed fully, " + + "interactive", inputs: []*proof.Proof{ makeProof(t, assetID1Tranche1), makeProof(t, assetID2Tranche1), }, + interactive: true, allocations: []*Allocation{ { Type: CommitAllocationToLocal, @@ -283,7 +480,66 @@ func TestDistributeCoins(t *testing.T) { }, }, { - name: "lots of assets", + name: "multiple assets, one consumed fully, " + + "non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID2Tranche1), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + }, + { + Type: CommitAllocationToRemote, + Amount: 1050, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 50, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 50, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + assetID2.ID(): { + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 1000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, + { + name: "lots of assets, interactive", inputs: []*proof.Proof{ makeProof(t, assetID1Tranche1), makeProof(t, assetID1Tranche2), @@ -295,6 +551,7 @@ func TestDistributeCoins(t *testing.T) { makeProof(t, assetID3Tranche2), makeProof(t, assetID3Tranche3), }, + interactive: true, allocations: []*Allocation{ { Type: CommitAllocationToLocal, @@ -357,13 +614,95 @@ func TestDistributeCoins(t *testing.T) { }, }, }, + { + name: "lots of assets, non-interactive", + inputs: []*proof.Proof{ + makeProof(t, assetID1Tranche1), + makeProof(t, assetID1Tranche2), + makeProof(t, assetID1Tranche3), + makeProof(t, assetID2Tranche1), + makeProof(t, assetID2Tranche2), + makeProof(t, assetID2Tranche3), + makeProof(t, assetID3Tranche1), + makeProof(t, assetID3Tranche2), + makeProof(t, assetID3Tranche3), + }, + interactive: false, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 3600, + }, + { + Type: CommitAllocationToRemote, + Amount: 63000, + OutputIndex: 1, + }, + }, + expectedInputs: map[asset.ID][]asset.ScriptKey{ + assetID1.ID(): { + assetID1Tranche1.ScriptKey, + assetID1Tranche2.ScriptKey, + assetID1Tranche3.ScriptKey, + }, + assetID2.ID(): { + assetID2Tranche1.ScriptKey, + assetID2Tranche2.ScriptKey, + assetID2Tranche3.ScriptKey, + }, + assetID3.ID(): { + assetID3Tranche1.ScriptKey, + assetID3Tranche2.ScriptKey, + assetID3Tranche3.ScriptKey, + }, + }, + expectedOutputs: map[asset.ID][]*tappsbt.VOutput{ + assetID1.ID(): { + { + Amount: 600, + Type: simple, + Interactive: false, + AnchorOutputIndex: 0, + }, + }, + assetID2.ID(): { + { + Amount: 3000, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 3000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + assetID3.ID(): { + { + Amount: 0, + Type: split, + Interactive: false, + AnchorOutputIndex: 0, + }, + { + Amount: 60000, + Type: simple, + Interactive: false, + AnchorOutputIndex: 1, + }, + }, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { packets, err := DistributeCoins( tc.inputs, tc.allocations, testParams, - tc.vPktVersion, + tc.interactive, tc.vPktVersion, ) require.NoError(t, err) @@ -410,3 +749,115 @@ func assertPackets(t *testing.T, packets []*tappsbt.VPacket, } } } + +// TestAllocatePiece tests the allocation of a piece of an asset. +func TestAllocatePiece(t *testing.T) { + tests := []struct { + name string + piece piece + allocation Allocation + toFill uint64 + interactive bool + expectedErr string + expectedAlloc uint64 + expectedOutput bool + expectedOutputType tappsbt.VOutputType + }{ + { + name: "valid allocation", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 50, + }, + toFill: 50, + interactive: true, + expectedAlloc: 50, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSimple, + }, + { + name: "allocation exceeds available", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 150, + }, + toFill: 150, + interactive: true, + expectedAlloc: 100, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSimple, + }, + { + name: "allocation with zero to fill", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 0, + }, + toFill: 0, + interactive: true, + expectedAlloc: 0, + expectedOutput: false, + }, + { + name: "allocation with split root", + piece: piece{ + assetID: asset.ID{1}, + totalAvailable: 100, + allocated: 0, + packet: &tappsbt.VPacket{}, + }, + allocation: Allocation{ + Amount: 50, + SplitRoot: true, + }, + toFill: 50, + interactive: false, + expectedAlloc: 50, + expectedOutput: true, + expectedOutputType: tappsbt.TypeSplitRoot, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + allocated, updatedPiece, err := allocatePiece( + tc.piece, tc.allocation, tc.toFill, + tc.interactive, + ) + if tc.expectedErr != "" { + require.ErrorContains(tt, err, tc.expectedErr) + return + } + + require.NoError(tt, err) + require.Equal(tt, tc.expectedAlloc, allocated) + require.Equal( + tt, tc.piece.totalAvailable-tc.expectedAlloc, + updatedPiece.available(), + ) + + if tc.expectedOutput { + require.Len(tt, updatedPiece.packet.Outputs, 1) + require.Equal( + tt, tc.expectedOutputType, + updatedPiece.packet.Outputs[0].Type, + ) + } + }) + } +} From d4661b7264f5d3914b16c5b2dece5022e3165f50 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:37 +0100 Subject: [PATCH 06/13] tapchannel+tapsend: add all vOutput fields to allocation Since we're going to be using the allocation code for all send logic, we need to add all fields of the vOutput to the allocation, so the values will be applied correctly. Since we also add the explicit LockTime, we need to rename the CLTV field so it's more clear that this field is only used for the HTLC tiebreaking sort procedure. Finally, we add a new Validate method to the allocation that makes sure all required input fields are set. --- tapchannel/commitment.go | 6 +-- tapsend/allocation.go | 69 +++++++++++++++++++++++++++++---- tapsend/allocation_sort.go | 2 +- tapsend/allocation_sort_test.go | 32 +++++++-------- tapsend/allocation_test.go | 10 +++++ 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 33faa8a27..08b4cf151 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -832,7 +832,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, // any TAP tweaks. htlcTree.TaprootKey, ), - CLTV: htlc.Timeout, + SortCLTV: htlc.Timeout, HtlcIndex: htlc.HtlcIndex, }) @@ -894,7 +894,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, SortTaprootKeyBytes: schnorr.SerializePubKey( htlcTree.TaprootKey, ), - CLTV: htlc.Timeout, + SortCLTV: htlc.Timeout, HtlcIndex: htlc.HtlcIndex, }) @@ -1365,7 +1365,7 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, // used for sorting _before_ applying any TAP tweaks. htlcTree.TaprootKey, ), - CLTV: htlcTimeout.UnwrapOr(0), + SortCLTV: htlcTimeout.UnwrapOr(0), HtlcIndex: htlcIndex, }} diff --git a/tapsend/allocation.go b/tapsend/allocation.go index 3fdddf5aa..413818c88 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -2,6 +2,7 @@ package tapsend import ( "bytes" + "errors" "fmt" "net/url" "sort" @@ -45,6 +46,12 @@ var ( // ErrCommitmentNotSet is an error that is returned if the output // commitment is not set for an allocation. ErrCommitmentNotSet = fmt.Errorf("output commitment not set") + + // ErrInvalidSibling is an error that is returned if both non-asset + // leaves and sibling preimage are set for an allocation. + ErrInvalidSibling = errors.New( + "both non-asset leaves and sibling preimage set", + ) ) // AllocationType is an enum that defines the different types of asset @@ -106,10 +113,21 @@ type Allocation struct { // NonAssetLeaves is the full list of TapLeaf nodes that aren't any // asset commitments. This is used to construct the tapscript sibling - // for the asset commitment. If this is a non-asset allocation and the - // list of leaves is empty, then we assume a BIP-0086 output. + // for the asset commitment. This is mutually exclusive to the + // SiblingPreimage field below, only one of them (or none) should be + // set. If this is a non-asset allocation and both NonAssetLeaves is + // empty and no SiblingPreimage is set, then we assume a BIP-0086 + // output. NonAssetLeaves []txscript.TapLeaf + // SiblingPreimage is the tapscript sibling preimage that is used to + // create the tapscript sibling for the asset commitment. This is + // mutually exclusive to the NonAssetLeaves above, only one of them (or + // none) should be set. If this is a non-asset allocation and both + // NonAssetLeaves is empty and no SiblingPreimage is set, then we assume + // a BIP-0086 output. + SiblingPreimage *commitment.TapscriptPreimage + // ScriptKey is the Taproot tweaked key encoding the different spend // conditions possible for the asset allocation. ScriptKey asset.ScriptKey @@ -131,16 +149,19 @@ type Allocation struct { // commitment present. This field should be used for sorting purposes. SortTaprootKeyBytes []byte - // CLTV is the CLTV timeout for the asset allocation. This is only - // relevant for sorting purposes and is expected to be zero for any + // SortCLTV is the SortCLTV timeout for the asset allocation. This is + // only relevant for sorting purposes and is expected to be zero for any // non-HTLC allocation. - CLTV uint32 + SortCLTV uint32 // Sequence is the CSV value for the asset allocation. This is only // relevant for HTLC second level transactions. This value will be set // as the relative time lock on the virtual output. Sequence uint32 + // LockTime is the actual CLTV value that will be set on the output. + LockTime uint64 + // HtlcIndex is the index of the HTLC that the allocation is for. This // is only relevant for HTLC allocations. HtlcIndex input.HtlcIndex @@ -152,15 +173,39 @@ type Allocation struct { // ProofDeliveryAddress is the address the proof courier should use to // upload the proof for this allocation. ProofDeliveryAddress *url.URL + + // AltLeaves represent data used to construct an Asset commitment, that + // will be inserted in the output anchor Tap commitment. These + // data-carrying leaves are used for a purpose distinct from + // representing individual Taproot Assets. + AltLeaves []asset.AltLeaf[asset.Asset] +} + +// Validate checks that the allocation is correctly set up and that the fields +// are consistent with each other. +func (a *Allocation) Validate() error { + // Make sure the two mutually exclusive fields aren't set at the same + // time. + if len(a.NonAssetLeaves) > 0 && a.SiblingPreimage != nil { + return ErrInvalidSibling + } + + return nil } // tapscriptSibling returns the tapscript sibling preimage from the non-asset // leaves of the allocation. If there are no non-asset leaves, nil is returned. func (a *Allocation) tapscriptSibling() (*commitment.TapscriptPreimage, error) { - if len(a.NonAssetLeaves) == 0 { + if len(a.NonAssetLeaves) == 0 && a.SiblingPreimage == nil { return nil, nil } + // The sibling preimage has precedence. Only one of the two fields + // should be set in any case. + if a.SiblingPreimage != nil { + return a.SiblingPreimage, nil + } + treeNodes, err := asset.TapTreeNodesFromLeaves(a.NonAssetLeaves) if err != nil { return nil, fmt.Errorf("error creating tapscript tree nodes: "+ @@ -239,7 +284,7 @@ func (a *Allocation) MatchesOutput(pkScript []byte, value int64, cltv uint32, } outputsEqual := bytes.Equal(pkScript, finalPkScript) && - value == int64(a.BtcAmount) && cltv == a.CLTV && + value == int64(a.BtcAmount) && cltv == a.SortCLTV && htlcIndex == a.HtlcIndex return outputsEqual, nil @@ -364,9 +409,15 @@ func DistributeCoins(inputs []*proof.Proof, allocations []*Allocation, inputSum += inputProof.Asset.Amount } - // Sum up the amounts that are to be allocated to the outputs. + // Sum up the amounts that are to be allocated to the outputs. We also + // validate that all the required fields are set and no conflicting + // fields are set. var outputSum uint64 for _, allocation := range allocations { + if err := allocation.Validate(); err != nil { + return nil, fmt.Errorf("invalid allocation: %w", err) + } + outputSum += allocation.Amount } @@ -483,7 +534,9 @@ func allocatePiece(p piece, a Allocation, toFill uint64, AnchorOutputTapscriptSibling: sibling, ScriptKey: a.ScriptKey, ProofDeliveryAddress: deliveryAddr, + LockTime: a.LockTime, RelativeLockTime: uint64(a.Sequence), + AltLeaves: a.AltLeaves, } // If we've allocated all pieces, or we don't need to allocate anything diff --git a/tapsend/allocation_sort.go b/tapsend/allocation_sort.go index 634b5d0ef..39def152d 100644 --- a/tapsend/allocation_sort.go +++ b/tapsend/allocation_sort.go @@ -28,7 +28,7 @@ func InPlaceAllocationSort(allocations []*Allocation) { bytes.Compare( i.SortTaprootKeyBytes, j.SortTaprootKeyBytes, ), - cmp.Compare(i.CLTV, j.CLTV), + cmp.Compare(i.SortCLTV, j.SortCLTV), cmp.Compare(i.HtlcIndex, j.HtlcIndex), ) }) diff --git a/tapsend/allocation_sort_test.go b/tapsend/allocation_sort_test.go index bfba13490..becf5ae75 100644 --- a/tapsend/allocation_sort_test.go +++ b/tapsend/allocation_sort_test.go @@ -21,90 +21,90 @@ func TestInPlaceAllocationSort(t *testing.T) { { BtcAmount: 2000, SortTaprootKeyBytes: []byte("b"), - CLTV: 300, + SortCLTV: 300, }, { BtcAmount: 3000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 200, + SortCLTV: 200, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 1, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 9, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 3, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, }, expected: []*Allocation{ { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("a"), - CLTV: 200, + SortCLTV: 200, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 1, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 3, }, { BtcAmount: 1000, SortTaprootKeyBytes: []byte("b"), - CLTV: 100, + SortCLTV: 100, HtlcIndex: 9, }, { BtcAmount: 2000, SortTaprootKeyBytes: []byte("b"), - CLTV: 300, + SortCLTV: 300, }, { BtcAmount: 3000, SortTaprootKeyBytes: []byte("a"), - CLTV: 100, + SortCLTV: 100, }, }, }, diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go index b18ff0ba8..f8ff51548 100644 --- a/tapsend/allocation_test.go +++ b/tapsend/allocation_test.go @@ -3,6 +3,7 @@ package tapsend import ( "testing" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" @@ -95,6 +96,15 @@ func TestDistributeCoinsErrors(t *testing.T) { }, testParams, true, tappsbt.V1, ) require.ErrorIs(t, err, ErrInputOutputSumMismatch) + + _, err = DistributeCoins( + []*proof.Proof{proofNormal}, []*Allocation{{ + Amount: assetNormal.Amount, + NonAssetLeaves: make([]txscript.TapLeaf, 1), + SiblingPreimage: &commitment.TapscriptPreimage{}, + }}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrInvalidSibling) } func TestDistributeCoins(t *testing.T) { From 771e681286e476ae555d461378331e4c4e6f3468 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:38 +0100 Subject: [PATCH 07/13] tapchannel+tapsend: fix script key handling Previously, we generated one allocation per channel asset. That was wrong and only worked because we never actually had multiple asset pieces in a channel. The idea of an allocation is that you only have one per on-chain output and that the different asset pieces are then distributed between the different outputs automatically. Because that means we need a way to give each virtual output of each asset piece a unique script key (where required), we need to turn the field into a functional type. In some places (e.g. the channel funding output) each vOutput of each different vPacket will have the same script key. In other places (e.g. cooperative close), we derived script keys per asset ID upfront so we need to use those for things to work correctly. --- tapchannel/aux_closer.go | 59 ++++++++++++++-------------- tapchannel/aux_sweeper.go | 52 +++++++++++++------------ tapchannel/commitment.go | 78 ++++++++++++++++++++------------------ tapsend/allocation.go | 48 +++++++++++++++++++++-- tapsend/allocation_test.go | 40 ++++++++++++++++--- 5 files changed, 178 insertions(+), 99 deletions(-) diff --git a/tapchannel/aux_closer.go b/tapchannel/aux_closer.go index 589827da2..b110ebd09 100644 --- a/tapchannel/aux_closer.go +++ b/tapchannel/aux_closer.go @@ -101,22 +101,28 @@ func NewAuxChanCloser(cfg AuxChanCloserCfg) *AuxChanCloser { } } -// createCloseAlloc is a helper function that creates an allocation for an -// asset close. -func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, +// createCloseAlloc is a helper function that creates an allocation for an asset +// close. This does not set a script key, as the script key will be set for each +// packet after the coins have been distributed. +func createCloseAlloc(isLocal, isInitiator bool, outputSum uint64, shutdownMsg tapchannelmsg.AuxShutdownMsg) (*tapsend.Allocation, error) { - assetID := closeAsset.ID() - // The sort pkScript for the allocation will just be the internal key, // mapped to a BIP 86 taproot output key. sortKeyBytes := txscript.ComputeTaprootKeyNoScript( shutdownMsg.AssetInternalKey.Val, ).SerializeCompressed() - scriptKey, ok := shutdownMsg.ScriptKeys.Val[assetID] - if !ok { - return nil, fmt.Errorf("no script key for asset %v", assetID) + scriptKeyGen := func(assetID asset.ID) (asset.ScriptKey, error) { + var emptyKey asset.ScriptKey + + scriptKey, ok := shutdownMsg.ScriptKeys.Val[assetID] + if !ok { + return emptyKey, fmt.Errorf("no script key for asset "+ + "%v", assetID) + } + + return asset.NewScriptKey(&scriptKey), nil } var proofDeliveryUrl *url.URL @@ -142,8 +148,8 @@ func createCloseAlloc(isLocal, isInitiator bool, closeAsset *asset.Asset, }(), SplitRoot: isInitiator, InternalKey: shutdownMsg.AssetInternalKey.Val, - ScriptKey: asset.NewScriptKey(&scriptKey), - Amount: closeAsset.Amount, + GenScriptKey: scriptKeyGen, + Amount: outputSum, AssetVersion: asset.V0, BtcAmount: tapsend.DummyAmtSats, SortTaprootKeyBytes: sortKeyBytes, @@ -260,37 +266,34 @@ func (a *AuxChanCloser) AuxCloseOutputs( localAlloc, remoteAlloc *tapsend.Allocation localAssetAnchorAmt, remoteAssetAnchorAmt btcutil.Amount ) - for _, localAssetProof := range commitState.LocalAssets.Val.Outputs { - localAsset := localAssetProof.Proof.Val.Asset - - closeAlloc, err := createCloseAlloc( - true, desc.Initiator, &localAsset, localShutdown, + sumAmounts := func(accu uint64, o *tapchannelmsg.AssetOutput) uint64 { + return accu + o.Amount.Val + } + localSum := fn.Reduce(commitState.LocalAssets.Val.Outputs, sumAmounts) + remoteSum := fn.Reduce(commitState.RemoteAssets.Val.Outputs, sumAmounts) + if localSum > 0 { + localAlloc, err = createCloseAlloc( + true, desc.Initiator, localSum, localShutdown, ) if err != nil { return none, err } - localAlloc = closeAlloc - - localAssetAnchorAmt += closeAlloc.BtcAmount + localAssetAnchorAmt += localAlloc.BtcAmount - closeAllocs = append(closeAllocs, closeAlloc) + closeAllocs = append(closeAllocs, localAlloc) } - for _, remoteAssetProof := range commitState.RemoteAssets.Val.Outputs { - remoteAsset := remoteAssetProof.Proof.Val.Asset - - closeAlloc, err := createCloseAlloc( - false, !desc.Initiator, &remoteAsset, remoteShutdown, + if remoteSum > 0 { + remoteAlloc, err = createCloseAlloc( + false, !desc.Initiator, remoteSum, remoteShutdown, ) if err != nil { return none, err } - remoteAlloc = closeAlloc - - remoteAssetAnchorAmt += closeAlloc.BtcAmount + remoteAssetAnchorAmt += remoteAlloc.BtcAmount - closeAllocs = append(closeAllocs, closeAlloc) + closeAllocs = append(closeAllocs, remoteAlloc) } // Next, we'll create allocations for the (up to) two settled outputs diff --git a/tapchannel/aux_sweeper.go b/tapchannel/aux_sweeper.go index b48e18f57..6b5fac818 100644 --- a/tapchannel/aux_sweeper.go +++ b/tapchannel/aux_sweeper.go @@ -233,38 +233,40 @@ func (a *AuxSweeper) createSweepVpackets(sweepInputs []*cmsg.AssetOutput, // Otherwise, for each out we want to sweep, we'll construct an // allocation that we'll use to deliver the funds back to the // wallet. - for _, localAsset := range sweepInputs { - // For each output, we'll need to create a new script - // key to use for the sweep transaction. + sweepAssetSum := tapchannelmsg.OutputSum(sweepInputs) + + // For this local allocation we'll need to create a new script + // key to use for the sweep transaction. + scriptKeyGen := func(asset.ID) (asset.ScriptKey, error) { + var emptyKey asset.ScriptKey + scriptKey, err := a.cfg.AddrBook.NextScriptKey( ctx, asset.TaprootAssetsKeyFamily, ) if err != nil { - return lfn.Err[[]*tappsbt.VPacket](err) + return emptyKey, err } - // With the script key created, we can make a new - // allocation that will be used to sweep the funds back - // to our wallet. - // - // We leave out the internal key here, as we'll make it - // later once we actually have the other set of inputs - // we need to sweep. - allocs = append(allocs, &tapsend.Allocation{ - Type: tapsend.CommitAllocationToLocal, - // We don't need to worry about sorting, as - // we'll always be the first output index in the - // transaction. - OutputIndex: 0, - Amount: localAsset.Amount.Val, - AssetVersion: asset.V1, - BtcAmount: tapsend.DummyAmtSats, - ScriptKey: scriptKey, - SortTaprootKeyBytes: schnorr.SerializePubKey( - scriptKey.PubKey, - ), - }) + return scriptKey, nil } + + // With the script key created, we can make a new allocation + // that will be used to sweep the funds back to our wallet. + // + // We leave out the internal key here, as we'll make it later + // once we actually have the other set of inputs we need to + // sweep. + allocs = append(allocs, &tapsend.Allocation{ + Type: tapsend.CommitAllocationToLocal, + // We don't need to worry about sorting, as + // we'll always be the first output index in the + // transaction. + OutputIndex: 0, + Amount: sweepAssetSum, + AssetVersion: asset.V1, + BtcAmount: tapsend.DummyAmtSats, + GenScriptKey: scriptKeyGen, + }) } log.Infof("Created %v allocations for commit tx sweep: %v", diff --git a/tapchannel/commitment.go b/tapchannel/commitment.go index 08b4cf151..bf7ead093 100644 --- a/tapchannel/commitment.go +++ b/tapchannel/commitment.go @@ -807,6 +807,17 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, schnorr.SerializePubKey(htlcTree.TaprootKey), schnorr.SerializePubKey(tweakedTree.TaprootKey)) + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + tweakedTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: tweakedTree.InternalKey, + }, + Tweak: tweakedTree.TapscriptRoot, + }, + } allocations = append(allocations, &tapsend.Allocation{ Type: allocType, Amount: rfqmsg.Sum(htlc.AssetBalances), @@ -815,17 +826,7 @@ func CreateAllocations(chanState lnwallet.AuxChanState, ourBalance, BtcAmount: htlc.Amount.ToSatoshis(), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - tweakedTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - PubKey: tweakedTree.InternalKey, - }, - Tweak: tweakedTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( // This _must_ remain the non-tweaked key, since // this is used for sorting _before_ applying @@ -1010,6 +1011,17 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + toLocalTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: toLocalTree.InternalKey, + }, + Tweak: toLocalTree.TapscriptRoot, + }, + } allocation := &tapsend.Allocation{ Type: tapsend.CommitAllocationToLocal, Amount: ourAssetBalance, @@ -1018,17 +1030,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, BtcAmount: ourBalance, InternalKey: toLocalTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - toLocalTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - PubKey: toLocalTree.InternalKey, - }, - Tweak: toLocalTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( toLocalTree.TaprootKey, ), @@ -1042,7 +1044,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, BtcAmount: ourBalance, InternalKey: toLocalTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey( + GenScriptKey: tapsend.StaticScriptPubKeyGen( toLocalTree.TaprootKey, ), SortTaprootKeyBytes: schnorr.SerializePubKey( @@ -1072,6 +1074,17 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, "sibling: %w", err) } + scriptKey := asset.ScriptKey{ + PubKey: asset.NewScriptKey( + toRemoteTree.TaprootKey, + ).PubKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: toRemoteTree.InternalKey, + }, + Tweak: toRemoteTree.TapscriptRoot, + }, + } allocation := &tapsend.Allocation{ Type: tapsend.CommitAllocationToRemote, Amount: theirAssetBalance, @@ -1080,18 +1093,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, BtcAmount: theirBalance, InternalKey: toRemoteTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.ScriptKey{ - PubKey: asset.NewScriptKey( - toRemoteTree.TaprootKey, - ).PubKey, - TweakedScriptKey: &asset.TweakedScriptKey{ - RawKey: keychain.KeyDescriptor{ - //nolint:lll - PubKey: toRemoteTree.InternalKey, - }, - Tweak: toRemoteTree.TapscriptRoot, - }, - }, + GenScriptKey: tapsend.StaticScriptKeyGen(scriptKey), SortTaprootKeyBytes: schnorr.SerializePubKey( toRemoteTree.TaprootKey, ), @@ -1105,7 +1107,7 @@ func addCommitmentOutputs(chanType channeldb.ChannelType, localChanCfg, BtcAmount: theirBalance, InternalKey: toRemoteTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey( + GenScriptKey: tapsend.StaticScriptPubKeyGen( toRemoteTree.TaprootKey, ), SortTaprootKeyBytes: schnorr.SerializePubKey( @@ -1359,7 +1361,9 @@ func createSecondLevelHtlcAllocations(chanType channeldb.ChannelType, ), InternalKey: htlcTree.InternalKey, NonAssetLeaves: sibling, - ScriptKey: asset.NewScriptKey(tweakedTree.TaprootKey), + GenScriptKey: tapsend.StaticScriptPubKeyGen( + tweakedTree.TaprootKey, + ), SortTaprootKeyBytes: schnorr.SerializePubKey( // This _must_ remain the non-tweaked key, since this is // used for sorting _before_ applying any TAP tweaks. diff --git a/tapsend/allocation.go b/tapsend/allocation.go index 413818c88..b212f2ecf 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -52,6 +52,12 @@ var ( ErrInvalidSibling = errors.New( "both non-asset leaves and sibling preimage set", ) + + // ErrScriptKeyGenMissing is an error that is returned if the script key + // generator function is not set. + ErrScriptKeyGenMissing = errors.New( + "script key generator function not set for asset allocation", + ) ) // AllocationType is an enum that defines the different types of asset @@ -87,6 +93,27 @@ const ( SecondLevelHtlcAllocation AllocationType = 5 ) +// ScriptKeyGen is a function type that is used for generating a script key for +// an asset specific script key. +type ScriptKeyGen func(assetID asset.ID) (asset.ScriptKey, error) + +// StaticScriptKeyGen is a helper function that returns a script key generator +// function that always returns the same script key. +func StaticScriptKeyGen(scriptKey asset.ScriptKey) ScriptKeyGen { + return func(asset.ID) (asset.ScriptKey, error) { + return scriptKey, nil + } +} + +// StaticScriptPubKeyGen is a helper function that returns a script key +// generator function that always returns the same script key, provided as a +// public key. +func StaticScriptPubKeyGen(scriptPubKey *btcec.PublicKey) ScriptKeyGen { + return func(asset.ID) (asset.ScriptKey, error) { + return asset.NewScriptKey(scriptPubKey), nil + } +} + // Allocation is a struct that tracks how many units of assets should be // allocated to a specific output of an on-chain transaction. An allocation can // be seen as a recipe/instruction to distribute a certain number of asset units @@ -128,9 +155,10 @@ type Allocation struct { // a BIP-0086 output. SiblingPreimage *commitment.TapscriptPreimage - // ScriptKey is the Taproot tweaked key encoding the different spend - // conditions possible for the asset allocation. - ScriptKey asset.ScriptKey + // GenScriptKey is a function that returns the Taproot tweaked key + // encoding the different spend conditions possible for the asset + // allocation for a certain asset ID. + GenScriptKey ScriptKeyGen // Amount is the amount of units that should be allocated in total. // Available units from different UTXOs are distributed up to this total @@ -190,6 +218,12 @@ func (a *Allocation) Validate() error { return ErrInvalidSibling } + // The script key generator function is required for any allocation that + // carries assets. + if a.Type != AllocationTypeNoAssets && a.GenScriptKey == nil { + return ErrScriptKeyGenMissing + } + return nil } @@ -525,6 +559,12 @@ func allocatePiece(p piece, a Allocation, toFill uint64, return 0, nil, err } + scriptKey, err := a.GenScriptKey(p.assetID) + if err != nil { + return 0, nil, fmt.Errorf("error generating script key for "+ + "allocation: %w", err) + } + deliveryAddr := a.ProofDeliveryAddress vOut := &tappsbt.VOutput{ AssetVersion: a.AssetVersion, @@ -532,7 +572,7 @@ func allocatePiece(p piece, a Allocation, toFill uint64, AnchorOutputIndex: a.OutputIndex, AnchorOutputInternalKey: a.InternalKey, AnchorOutputTapscriptSibling: sibling, - ScriptKey: a.ScriptKey, + ScriptKey: scriptKey, ProofDeliveryAddress: deliveryAddr, LockTime: a.LockTime, RelativeLockTime: uint64(a.Sequence), diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go index f8ff51548..3b77ca472 100644 --- a/tapsend/allocation_test.go +++ b/tapsend/allocation_test.go @@ -92,6 +92,9 @@ func TestDistributeCoinsErrors(t *testing.T) { []*proof.Proof{proofNormal}, []*Allocation{ { Amount: assetNormal.Amount / 2, + GenScriptKey: StaticScriptKeyGen( + asset.RandScriptKey(t), + ), }, }, testParams, true, tappsbt.V1, ) @@ -105,6 +108,14 @@ func TestDistributeCoinsErrors(t *testing.T) { }}, testParams, true, tappsbt.V1, ) require.ErrorIs(t, err, ErrInvalidSibling) + + _, err = DistributeCoins( + []*proof.Proof{proofNormal}, []*Allocation{{ + Type: CommitAllocationToLocal, + Amount: assetNormal.Amount, + }}, testParams, true, tappsbt.V1, + ) + require.ErrorIs(t, err, ErrScriptKeyGenMissing) } func TestDistributeCoins(t *testing.T) { @@ -710,6 +721,19 @@ func TestDistributeCoins(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + // We don't care about script keys in this test so we + // just set static ones. + dummyScriptKey := asset.RandScriptKey(t) + scriptKeyGen := StaticScriptKeyGen(dummyScriptKey) + for _, allocation := range tc.allocations { + allocation.GenScriptKey = scriptKeyGen + } + for _, outputs := range tc.expectedOutputs { + for _, output := range outputs { + output.ScriptKey = dummyScriptKey + } + } + packets, err := DistributeCoins( tc.inputs, tc.allocations, testParams, tc.interactive, tc.vPktVersion, @@ -762,6 +786,8 @@ func assertPackets(t *testing.T, packets []*tappsbt.VPacket, // TestAllocatePiece tests the allocation of a piece of an asset. func TestAllocatePiece(t *testing.T) { + dummyScriptKey := asset.RandScriptKey(t) + scriptKeyGen := StaticScriptKeyGen(dummyScriptKey) tests := []struct { name string piece piece @@ -782,7 +808,8 @@ func TestAllocatePiece(t *testing.T) { packet: &tappsbt.VPacket{}, }, allocation: Allocation{ - Amount: 50, + Amount: 50, + GenScriptKey: scriptKeyGen, }, toFill: 50, interactive: true, @@ -799,7 +826,8 @@ func TestAllocatePiece(t *testing.T) { packet: &tappsbt.VPacket{}, }, allocation: Allocation{ - Amount: 150, + Amount: 150, + GenScriptKey: scriptKeyGen, }, toFill: 150, interactive: true, @@ -816,7 +844,8 @@ func TestAllocatePiece(t *testing.T) { packet: &tappsbt.VPacket{}, }, allocation: Allocation{ - Amount: 0, + Amount: 0, + GenScriptKey: scriptKeyGen, }, toFill: 0, interactive: true, @@ -832,8 +861,9 @@ func TestAllocatePiece(t *testing.T) { packet: &tappsbt.VPacket{}, }, allocation: Allocation{ - Amount: 50, - SplitRoot: true, + Amount: 50, + SplitRoot: true, + GenScriptKey: scriptKeyGen, }, toFill: 50, interactive: false, From fae7fe96e765d50e844d14870a6ec37aed5f0c88 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:40 +0100 Subject: [PATCH 08/13] tapsend: add AllocationsFromTemplate helper function Because we'll need to convert a funding template that's potentially given to us over the RPC interface into an allocation, we need to add custom code for that. This will be used in the freighter to interpret the user-provided virtual packet into an allocation in the normal send logic. --- tapsend/allocation.go | 105 +++++++++++++++++++++++ tapsend/allocation_test.go | 169 +++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) diff --git a/tapsend/allocation.go b/tapsend/allocation.go index b212f2ecf..d28c3b6e3 100644 --- a/tapsend/allocation.go +++ b/tapsend/allocation.go @@ -697,3 +697,108 @@ func NonAssetExclusionProofs( return nil } } + +// AllocationsFromTemplate creates a list of allocations from a spend template. +// If there is no split output present in the template, one is created to carry +// potential change or a zero-value tombstone output in case of a +// non-interactive transfer. The script key for those change/tombstone outputs +// are set to the NUMS script key and need to be replaced with an actual script +// key (if the change is non-zero) after the coin distribution has been +// performed. +func AllocationsFromTemplate(tpl *tappsbt.VPacket, + inputSum uint64) ([]*Allocation, bool, error) { + + if len(tpl.Outputs) == 0 { + return nil, false, fmt.Errorf("spend template has no outputs") + } + + // We first detect if the outputs are interactive or not. They need to + // all say the same thing, otherwise we can't proceed. + isInteractive := tpl.Outputs[0].Interactive + for idx := 1; idx < len(tpl.Outputs); idx++ { + if tpl.Outputs[idx].Interactive != isInteractive { + return nil, false, fmt.Errorf("outputs have " + + "different interactive flags") + } + } + + // Calculate the total amount that is being spent. + var outputAmount uint64 + for _, out := range tpl.Outputs { + outputAmount += out.Amount + } + + // Validate the change amount so we can use it later. + if outputAmount > inputSum { + return nil, false, fmt.Errorf("output amount exceeds input sum") + } + changeAmount := inputSum - outputAmount + + // In case there is no change/tombstone output, we assume the anchor + // output indexes are still increasing. So we'll just use the next one + // after the last output's anchor output index. + splitOutIndex := tpl.Outputs[len(tpl.Outputs)-1].AnchorOutputIndex + 1 + + // If there is no change/tombstone output in the template, we always + // create one. This will not be used (e.g. turned into an actual virtual + // output) by the allocation logic if is not needed (when it's an + // interactive full-value send). + localAllocation := &Allocation{ + Type: CommitAllocationToLocal, + OutputIndex: splitOutIndex, + SplitRoot: true, + GenScriptKey: StaticScriptKeyGen(asset.NUMSScriptKey), + } + + // If we have a split root defined in the template, we'll use that as + // the template for the local allocation. + if tpl.HasSplitRootOutput() { + splitRootOut, err := tpl.SplitRootOutput() + if err != nil { + return nil, false, err + } + + setAllocationFieldsFromOutput(localAllocation, splitRootOut) + } + + // We do need to overwrite the amount of the local allocation with the + // change amount now. We do _NOT_, however, derive change script keys + // yet, since we don't know if some of the packets created by the coin + // distribution might remain an un-spendable zero-amount tombstone + // output, and we don't want to derive change script keys for those. + localAllocation.Amount = changeAmount + + // We now create the remote allocations for each non-split output. + remoteAllocations := make([]*Allocation, 0, len(tpl.Outputs)) + normalOuts := fn.Filter(tpl.Outputs, tappsbt.VOutIsNotSplitRoot) + for _, out := range normalOuts { + remoteAllocation := &Allocation{ + Type: CommitAllocationToRemote, + SplitRoot: false, + } + + setAllocationFieldsFromOutput(remoteAllocation, out) + remoteAllocations = append(remoteAllocations, remoteAllocation) + } + + allAllocations := append( + []*Allocation{localAllocation}, remoteAllocations..., + ) + + return allAllocations, isInteractive, nil +} + +// setAllocationFieldsFromOutput sets the fields of the given allocation from +// the given virtual output. +func setAllocationFieldsFromOutput(alloc *Allocation, vOut *tappsbt.VOutput) { + alloc.Amount = vOut.Amount + alloc.AssetVersion = vOut.AssetVersion + alloc.OutputIndex = vOut.AnchorOutputIndex + alloc.InternalKey = vOut.AnchorOutputInternalKey + alloc.GenScriptKey = StaticScriptKeyGen(vOut.ScriptKey) + alloc.Sequence = uint32(vOut.RelativeLockTime) + alloc.LockTime = vOut.LockTime + alloc.ProofDeliveryAddress = vOut.ProofDeliveryAddress + alloc.AltLeaves = vOut.AltLeaves + alloc.SiblingPreimage = vOut.AnchorOutputTapscriptSibling +} diff --git a/tapsend/allocation_test.go b/tapsend/allocation_test.go index 3b77ca472..13d59b1b8 100644 --- a/tapsend/allocation_test.go +++ b/tapsend/allocation_test.go @@ -901,3 +901,172 @@ func TestAllocatePiece(t *testing.T) { }) } } + +// TestAllocationsFromTemplate tests that we can correctly turn a virtual packet +// template in a set of allocations. +func TestAllocationsFromTemplate(t *testing.T) { + t.Parallel() + + dummyScriptKey := asset.RandScriptKey(t) + dummyAltLeaves := asset.ToAltLeaves(asset.RandAltLeaves(t, true)) + + var ( + simple = tappsbt.TypeSimple + split = tappsbt.TypeSplitRoot + ) + testCases := []struct { + name string + template *tappsbt.VPacket + inputSum uint64 + expectErr string + interactive bool + allocations []*Allocation + }{ + { + name: "no outputs", + template: &tappsbt.VPacket{}, + expectErr: "spend template has no outputs", + }, + { + name: "mixed interactive and non-interactive", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Interactive: true, + }, + { + Interactive: false, + }, + }, + }, + expectErr: "different interactive flags", + }, + { + name: "output greater than input", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Amount: 100, + }, + { + Amount: 200, + }, + }, + }, + inputSum: 100, + expectErr: "output amount exceeds input sum", + }, + { + name: "single asset, split, interactive, no " + + "change", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Amount: 50, + ScriptKey: dummyScriptKey, + Interactive: true, + AltLeaves: dummyAltLeaves, + }, + }, + }, + inputSum: 100, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + OutputIndex: 1, + GenScriptKey: StaticScriptKeyGen( + asset.NUMSScriptKey, + ), + }, + { + Type: CommitAllocationToRemote, + Amount: 50, + GenScriptKey: StaticScriptKeyGen( + dummyScriptKey, + ), + AltLeaves: dummyAltLeaves, + }, + }, + }, + { + name: "single asset, split, interactive, w/ " + + "change", + template: &tappsbt.VPacket{ + Outputs: []*tappsbt.VOutput{ + { + Type: split, + Interactive: true, + }, + { + Type: simple, + Interactive: true, + Amount: 50, + AnchorOutputIndex: 1, + }, + }, + }, + inputSum: 100, + interactive: true, + allocations: []*Allocation{ + { + Type: CommitAllocationToLocal, + SplitRoot: true, + Amount: 50, + OutputIndex: 0, + }, + { + Type: CommitAllocationToRemote, + Amount: 50, + OutputIndex: 1, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + allocs, interactive, err := AllocationsFromTemplate( + tc.template, tc.inputSum, + ) + + if tc.expectErr != "" { + require.Contains(t, err.Error(), tc.expectErr) + return + } + + require.NoError(t, err) + + require.Equal(t, tc.interactive, interactive) + + // We first check that the allocations return the + // correct script key from their generator. + for idx := range allocs { + // If the script key doesn't matter, we can + // skip this part and also ignore the generator + // in the comparison. + if tc.allocations[idx].GenScriptKey == nil { + allocs[idx].GenScriptKey = nil + + continue + } + + var id asset.ID + expected, _ := tc.allocations[idx].GenScriptKey( + id, + ) + actual, _ := allocs[idx].GenScriptKey(id) + require.Equal(t, expected, actual) + + // We then remove the generator from the + // allocation to make it easier to compare. + allocs[idx].GenScriptKey = nil + tc.allocations[idx].GenScriptKey = nil + } + + require.Equal(t, tc.allocations, allocs) + }) + } +} From 825add12f26f5052d50716d5e49bca7c0cd363e6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:41 +0100 Subject: [PATCH 09/13] tapfreighter: extract PrevID method --- tapfreighter/fund.go | 9 +-------- tapfreighter/interface.go | 9 +++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tapfreighter/fund.go b/tapfreighter/fund.go index eb2f6396f..0da5deb2e 100644 --- a/tapfreighter/fund.go +++ b/tapfreighter/fund.go @@ -473,15 +473,8 @@ func createAndSetInput(vPkt *tappsbt.VPacket, idx int, // At this point, we have a valid "coin" to spend in the commitment, so // we'll add the relevant information to the virtual TX's input. - prevID := asset.PrevID{ - OutPoint: assetInput.AnchorPoint, - ID: assetInput.Asset.ID(), - ScriptKey: asset.ToSerialized( - assetInput.Asset.ScriptKey.PubKey, - ), - } vPkt.Inputs[idx] = &tappsbt.VInput{ - PrevID: prevID, + PrevID: assetInput.PrevID(), Anchor: tappsbt.Anchor{ Value: assetInput.AnchorOutputValue, PkScript: anchorPkScript, diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index b16131abc..9c1f78f8a 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -97,6 +97,15 @@ type AnchoredCommitment struct { Asset *asset.Asset } +// PrevID returns the previous ID of the asset commitment. +func (c *AnchoredCommitment) PrevID() asset.PrevID { + return asset.PrevID{ + OutPoint: c.AnchorPoint, + ID: c.Asset.ID(), + ScriptKey: asset.ToSerialized(c.Asset.ScriptKey.PubKey), + } +} + var ( // ErrMatchingAssetsNotFound is returned when an instance of // AssetStoreListCoins cannot satisfy the given asset identification From bf69bc65827fee0cee38bc26bcd24b279df956e2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:42 +0100 Subject: [PATCH 10/13] tapfreighter: make funding multi package compatible --- tapfreighter/fund.go | 253 +++++++++++-------------- tapfreighter/fund_test.go | 380 ++++++++++++++++++++++++++++++++++++-- tapfreighter/log.go | 8 + 3 files changed, 479 insertions(+), 162 deletions(-) diff --git a/tapfreighter/fund.go b/tapfreighter/fund.go index 0da5deb2e..41130f588 100644 --- a/tapfreighter/fund.go +++ b/tapfreighter/fund.go @@ -13,10 +13,10 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" - "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/tapsend" + "golang.org/x/exp/maps" ) // createFundedPacketWithInputs funds a set of virtual transaction with the @@ -25,80 +25,139 @@ import ( // single asset ID/tranche or group key with multiple tranches). func createFundedPacketWithInputs(ctx context.Context, exporter proof.Exporter, keyRing KeyRing, addrBook AddrBook, fundDesc *tapsend.FundingDescriptor, - vPkt *tappsbt.VPacket, + vPktTemplate *tappsbt.VPacket, selectedCommitments []*AnchoredCommitment) (*FundedVPacket, error) { - if vPkt.ChainParams == nil { + if vPktTemplate.ChainParams == nil { return nil, errors.New("chain params not set in virtual packet") } + chainParams := vPktTemplate.ChainParams log.Infof("Selected %v asset inputs for send of %d to %s", len(selectedCommitments), fundDesc.Amount, &fundDesc.AssetSpecifier) - assetType := selectedCommitments[0].Asset.Type - - totalInputAmt := uint64(0) + var inputSum uint64 + inputProofs := make( + map[asset.PrevID]*proof.Proof, len(selectedCommitments), + ) + selectedCommitmentsByPrevID := make( + map[asset.PrevID]*AnchoredCommitment, len(selectedCommitments), + ) for _, anchorAsset := range selectedCommitments { - // We only use the sum of all assets of the same TAP commitment - // key to avoid counting passive assets as well. We'll filter - // out the passive assets from the selected commitments in a - // later step. + // We only use the inputs of assets of the same TAP commitment + // as we want to fund for. These are the active assets that + // we're going to distribute. All other assets are passive and + // will be detected and added later. if anchorAsset.Asset.TapCommitmentKey() != fundDesc.TapCommitmentKey() { continue } - totalInputAmt += anchorAsset.Asset.Amount + // We'll also include an inclusion proof for the input asset in + // the virtual transaction. With that a signer can verify that + // the asset was actually committed to in the anchor output. + inputProof, err := fetchInputProof( + ctx, exporter, anchorAsset.Asset, + anchorAsset.AnchorPoint, + ) + if err != nil { + return nil, fmt.Errorf("error fetching input proof: %w", + err) + } + + inputSum += anchorAsset.Asset.Amount + inputProofs[anchorAsset.PrevID()] = inputProof + selectedCommitmentsByPrevID[anchorAsset.PrevID()] = anchorAsset } - inputCommitments, err := setVPacketInputs( - ctx, exporter, selectedCommitments, vPkt, - ) + // We try to identify and annotate any script keys in the template that + // might be ours. + err := annotateLocalScriptKeys(ctx, vPktTemplate, addrBook) if err != nil { - return nil, err + return nil, fmt.Errorf("error annotating local script "+ + "keys: %w", err) } - fullValue, err := tapsend.ValidateInputs( - inputCommitments, assetType, fundDesc.AssetSpecifier, - fundDesc.Amount, + allocations, interactive, err := tapsend.AllocationsFromTemplate( + vPktTemplate, inputSum, ) if err != nil { - return nil, err + return nil, fmt.Errorf("error extracting allocations: %w", err) } - // Make sure we'll recognize local script keys in the virtual packet - // later on in the process by annotating them with the full descriptor - // information. - if err := annotateLocalScriptKeys(ctx, vPkt, addrBook); err != nil { - return nil, err + allPackets, err := tapsend.DistributeCoins( + maps.Values(inputProofs), allocations, chainParams, interactive, + vPktTemplate.Version, + ) + if err != nil { + return nil, fmt.Errorf("unable to distribute coins: %w", err) } - // If we don't spend the full value, we need to create a change output. - changeAmount := totalInputAmt - fundDesc.Amount - err = createChangeOutput(ctx, vPkt, keyRing, fullValue, changeAmount) - if err != nil { - return nil, err + // Add all the input information to the virtual packets and also make + // sure we have proper change output keys for non-zero change outputs. + for _, vPkt := range allPackets { + for idx := range vPkt.Inputs { + prevID := vPkt.Inputs[idx].PrevID + assetInput, ok := selectedCommitmentsByPrevID[prevID] + if !ok { + return nil, fmt.Errorf("input commitment not "+ + "found for prevID %v", prevID) + } + + inputProof, ok := inputProofs[prevID] + if !ok { + return nil, fmt.Errorf("input proof not found "+ + "for prevID %v", prevID) + } + + err = createAndSetInput( + vPkt, idx, assetInput, inputProof, + ) + if err != nil { + return nil, fmt.Errorf("unable to create and "+ + "set input: %w", err) + } + } + + err = deriveChangeOutputKey(ctx, vPkt, keyRing) + if err != nil { + return nil, fmt.Errorf("unable to derive change "+ + "output key: %w", err) + } } // Before we can prepare output assets for our send, we need to generate // a new internal key for the anchor outputs. We assume any output that // hasn't got an internal key set is going to a local anchor, and we // provide the internal key for that. - packets := []*tappsbt.VPacket{vPkt} - err = generateOutputAnchorInternalKeys(ctx, packets, keyRing) + err = generateOutputAnchorInternalKeys(ctx, allPackets, keyRing) if err != nil { return nil, fmt.Errorf("unable to generate output anchor "+ "internal keys: %w", err) } - if err := tapsend.PrepareOutputAssets(ctx, vPkt); err != nil { - return nil, fmt.Errorf("unable to prepare outputs: %w", err) + for _, vPkt := range allPackets { + if err := tapsend.PrepareOutputAssets(ctx, vPkt); err != nil { + log.Errorf("Error preparing output assets: %v, "+ + "packets: %v", err, limitSpewer.Sdump(vPkt)) + return nil, fmt.Errorf("unable to prepare outputs: %w", + err) + } + } + + // Extract just the TAP commitments by input from the selected anchored + // commitments. + inputCommitments := make( + tappsbt.InputCommitments, len(selectedCommitmentsByPrevID), + ) + for prevID, anchorAsset := range selectedCommitmentsByPrevID { + inputCommitments[prevID] = anchorAsset.Commitment } return &FundedVPacket{ - VPackets: packets, + VPackets: allPackets, InputCommitments: inputCommitments, }, nil } @@ -138,56 +197,35 @@ func annotateLocalScriptKeys(ctx context.Context, vPkt *tappsbt.VPacket, return nil } -// createChangeOutput creates a change output for the given virtual packet if -// it isn't fully spent. -func createChangeOutput(ctx context.Context, vPkt *tappsbt.VPacket, - keyRing KeyRing, fullValue bool, changeAmount uint64) error { - - // If we're spending the full value, we don't need a change output. We - // currently assume that if it's a full-value non-interactive spend that - // the packet was created with the correct function in the tappsbt - // packet that adds the NUMS script key output for the tombstone. If - // the user doesn't set that, then an error will be returned from the - // tapsend.PrepareOutputAssets function. But we should probably change - // that and allow the user to specify a minimum packet template and add - // whatever else is needed to it automatically. - if fullValue { +// deriveChangeOutputKey makes sure the change output has a proper key that goes +// back to the local node, assuming there is a change output and it isn't a +// zero-value tombstone. +func deriveChangeOutputKey(ctx context.Context, vPkt *tappsbt.VPacket, + keyRing KeyRing) error { + + // If we don't have a split output then there's no change. + if !vPkt.HasSplitRootOutput() { return nil } - // We expect some change back, or have passive assets to commit to, so - // let's make sure we create a transfer output. changeOut, err := vPkt.SplitRootOutput() if err != nil { - lastOut := vPkt.Outputs[len(vPkt.Outputs)-1] - splitOutIndex := lastOut.AnchorOutputIndex + 1 - changeOut = &tappsbt.VOutput{ - Type: tappsbt.TypeSplitRoot, - Interactive: lastOut.Interactive, - AnchorOutputIndex: splitOutIndex, - - // We want to handle deriving a real key in a - // generic manner, so we'll do that just below. - ScriptKey: asset.NUMSScriptKey, - } - - vPkt.Outputs = append(vPkt.Outputs, changeOut) + return err } - // Since we know we're going to receive some change back, we - // need to make sure it is going to an address that we control. - // This should only be the case where we create the default - // change output with the NUMS key to avoid deriving too many - // keys prematurely. We don't need to derive a new key if we - // only have passive assets to commit to, since they all have - // their own script key and the output is more of a placeholder - // to attach the passive assets to. + // Since we know we're going to receive some change back, we need to + // make sure it is going to an address that we control. This should only + // be the case where we create the default change output with the NUMS + // key to avoid deriving too many keys prematurely. We don't need to + // derive a new key if we only have passive assets to commit to, since + // they all have their own script key and the output is more of a + // placeholder to attach the passive assets to. unSpendable, err := changeOut.ScriptKey.IsUnSpendable() if err != nil { return fmt.Errorf("cannot determine if script key is "+ "spendable: %w", err) } - if unSpendable { + if unSpendable && changeOut.Amount > 0 { changeScriptKey, err := keyRing.DeriveNextKey( ctx, asset.TaprootAssetsKeyFamily, ) @@ -202,30 +240,6 @@ func createChangeOutput(ctx context.Context, vPkt *tappsbt.VPacket, ) } - // For existing change outputs, we'll just update the amount - // since we might not have known what coin would've been - // selected and how large the change would turn out to be. - changeOut.Amount = changeAmount - - // The asset version of the output should be the max of the set - // of input versions. We need to set this now as in - // PrepareOutputAssets locators are created which includes the - // version from the vOut. If we don't set it here, a v1 asset - // spent that becomes change will be a v0 if combined with such - // inputs. - // - // TODO(roasbeef): remove as not needed? - maxVersion := func(maxVersion asset.Version, - vInput *tappsbt.VInput) asset.Version { - - if vInput.Asset().Version > maxVersion { - return vInput.Asset().Version - } - - return maxVersion - } - changeOut.AssetVersion = fn.Reduce(vPkt.Inputs, maxVersion) - return nil } @@ -357,53 +371,6 @@ func generateOutputAnchorInternalKeys(ctx context.Context, return nil } -// setVPacketInputs sets the inputs of the given vPkt to the given send eligible -// commitments. It also returns the assets that were used as inputs. -func setVPacketInputs(ctx context.Context, exporter proof.Exporter, - eligibleCommitments []*AnchoredCommitment, - vPkt *tappsbt.VPacket) (tappsbt.InputCommitments, error) { - - vPkt.Inputs = make([]*tappsbt.VInput, len(eligibleCommitments)) - inputCommitments := make(tappsbt.InputCommitments) - - for idx := range eligibleCommitments { - // If the key found for the input UTXO cannot be identified as - // belonging to the lnd wallet, we won't be able to sign for it. - // This would happen if a user manually imported an asset that - // was issued/received for/on another node. We should probably - // not create asset entries for such imported assets in the - // first place, as we won't be able to spend it anyway. But for - // now we just put this check in place. - assetInput := eligibleCommitments[idx] - - // We'll also include an inclusion proof for the input asset in - // the virtual transaction. With that a signer can verify that - // the asset was actually committed to in the anchor output. - inputProof, err := fetchInputProof( - ctx, exporter, assetInput.Asset, assetInput.AnchorPoint, - ) - if err != nil { - return nil, fmt.Errorf("error fetching input proof: %w", - err) - } - - // Create the virtual packet input including the chain anchor - // information. - err = createAndSetInput( - vPkt, idx, assetInput, inputProof, - ) - if err != nil { - return nil, fmt.Errorf("unable to create and set "+ - "input: %w", err) - } - - prevID := vPkt.Inputs[idx].PrevID - inputCommitments[prevID] = assetInput.Commitment - } - - return inputCommitments, nil -} - // createAndSetInput creates a virtual packet input for the given asset input // and sets it on the given virtual packet. func createAndSetInput(vPkt *tappsbt.VPacket, idx int, diff --git a/tapfreighter/fund_test.go b/tapfreighter/fund_test.go index ed200cf5d..04447a8f1 100644 --- a/tapfreighter/fund_test.go +++ b/tapfreighter/fund_test.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/internal/test" "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -32,13 +33,20 @@ var ( ) type mockExporter struct { - singleProof proof.Proof + proofs []*proof.Proof } -func (m *mockExporter) FetchProof(context.Context, - proof.Locator) (proof.Blob, error) { +func (m *mockExporter) FetchProof(_ context.Context, + loc proof.Locator) (proof.Blob, error) { - f, err := proof.NewFile(proof.V0, m.singleProof) + singleProof, err := fn.First(m.proofs, func(p *proof.Proof) bool { + return p.Asset.ScriptKey.PubKey.IsEqual(&loc.ScriptKey) + }) + if err != nil { + return nil, err + } + + f, err := proof.NewFile(proof.V0, *singleProof) if err != nil { return nil, err } @@ -81,8 +89,8 @@ func (m *mockAddrBook) FetchInternalKeyLocator(_ context.Context, var _ AddrBook = (*mockAddrBook)(nil) -func randProof(t *testing.T, amount uint64, - internalKey keychain.KeyDescriptor) proof.Proof { +func randProof(t *testing.T, amount uint64, internalKey keychain.KeyDescriptor, + groupKey *asset.GroupKey) proof.Proof { oddTxBlockHex, err := os.ReadFile(oddTxBlockHexFileName) require.NoError(t, err) @@ -101,7 +109,7 @@ func randProof(t *testing.T, amount uint64, txMerkleProof := proof.TxMerkleProof{} mintCommitment, assets, err := commitment.Mint( - nil, randGen, nil, &commitment.AssetDetails{ + nil, randGen, groupKey, &commitment.AssetDetails{ Type: randGen.Type, ScriptKey: test.PubToKeyDesc(scriptKey), Amount: &amount, @@ -240,10 +248,12 @@ func TestFundPacket(t *testing.T) { ctx := context.Background() internalKey, _ := test.RandKeyDesc(t) + grpInternalKey1, _ := test.RandKeyDesc(t) + grpInternalKey2, _ := test.RandKeyDesc(t) scriptKey := asset.RandScriptKey(t) const mintAmount = 500 - inputProof := randProof(t, mintAmount, internalKey) + inputProof := randProof(t, mintAmount, internalKey, nil) inputAsset := inputProof.Asset assetID := inputAsset.ID() @@ -256,10 +266,48 @@ func TestFundPacket(t *testing.T) { inputCommitment, err := commitment.FromAssets(nil, &inputProof.Asset) require.NoError(t, err) + groupPubKey := test.RandPubKey(t) + groupKey := &asset.GroupKey{ + GroupPubKey: *groupPubKey, + } + + groupProof1 := randProof(t, mintAmount*2, grpInternalKey1, groupKey) + groupInputAsset1 := groupProof1.Asset + groupAssetID1 := groupInputAsset1.ID() + + groupProof2 := randProof(t, mintAmount*2, grpInternalKey2, groupKey) + groupInputAsset2 := groupProof2.Asset + groupAssetID2 := groupInputAsset2.ID() + + grpInputPrevID1 := asset.PrevID{ + OutPoint: groupProof1.OutPoint(), + ID: groupAssetID1, + ScriptKey: asset.ToSerialized( + groupInputAsset1.ScriptKey.PubKey, + ), + } + grpInputPrevID2 := asset.PrevID{ + OutPoint: groupProof2.OutPoint(), + ID: groupAssetID2, + ScriptKey: asset.ToSerialized( + groupInputAsset2.ScriptKey.PubKey, + ), + } + + grpInputCommitment1, err := commitment.FromAssets( + nil, &groupInputAsset1, + ) + require.NoError(t, err) + grpInputCommitment2, err := commitment.FromAssets( + nil, &groupInputAsset2, + ) + require.NoError(t, err) + testCases := []struct { name string fundDesc *tapsend.FundingDescriptor vPkt *tappsbt.VPacket + inputProofs []*proof.Proof selectedCommitments []*AnchoredCommitment keysDerived int expectedErr string @@ -283,6 +331,7 @@ func TestFundPacket(t *testing.T) { Interactive: false, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, @@ -297,21 +346,21 @@ func TestFundPacket(t *testing.T) { r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { pkt0Outputs := []*tappsbt.VOutput{{ - Amount: 20, - Type: tappsbt.TypeSimple, - ScriptKey: scriptKey, + Amount: mintAmount - 20, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), AnchorOutputInternalKey: r.PubKeyAt( t, 1, ), - AnchorOutputIndex: 0, + AnchorOutputIndex: 1, }, { - Amount: mintAmount - 20, - Type: tappsbt.TypeSplitRoot, - ScriptKey: r.ScriptKeyAt(t, 0), + Amount: 20, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, AnchorOutputInternalKey: r.PubKeyAt( t, 2, ), - AnchorOutputIndex: 1, + AnchorOutputIndex: 0, }} return [][]*tappsbt.VOutput{pkt0Outputs} @@ -333,14 +382,40 @@ func TestFundPacket(t *testing.T) { Interactive: false, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, Commitment: inputCommitment, Asset: &inputAsset, }}, - keysDerived: 1, - expectedErr: "single output must be interactive", + keysDerived: 2, + expectedInputCommitments: tappsbt.InputCommitments{ + inputPrevID: inputCommitment, + }, + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }} + + return [][]*tappsbt.VOutput{pkt0Outputs} + }, }, { name: "single input, full value, change present", @@ -364,6 +439,7 @@ func TestFundPacket(t *testing.T) { AnchorOutputIndex: 1, }}, }, + inputProofs: []*proof.Proof{&inputProof}, selectedCommitments: []*AnchoredCommitment{{ AnchorPoint: inputProof.OutPoint(), InternalKey: internalKey, @@ -398,12 +474,278 @@ func TestFundPacket(t *testing.T) { return [][]*tappsbt.VOutput{pkt0Outputs} }, }, + { + name: "multi input, full value, change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 4, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Type: tappsbt.TypeSplitRoot, + Amount: 0, + ScriptKey: asset.NUMSScriptKey, + Interactive: false, + }, { + Amount: mintAmount * 4, + ScriptKey: scriptKey, + Interactive: false, + AnchorOutputIndex: 1, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 2, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. In the first vOutput, we + // always each have the tombstone zero-value output, + // since this is a full-value spend across two vPackets. + // The vOutputs across the two vPackets should each go + // to the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 0, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }} + + // Actually, the two vPackets should be the + // same, just different inputs. + require.Equal(t, pkt0Outputs, pkt1Outputs) + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, + { + name: "multi input, partial amount, no change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 3, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Amount: mintAmount * 3, + ScriptKey: scriptKey, + Interactive: false, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 3, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. In the first vOutput, we + // always each have the tombstone zero-value output, + // since this is a full-value spend across two vPackets. + // The vOutputs across the two vPackets should each go + // to the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: mintAmount, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 0, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 1, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 0, + }} + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, + { + name: "multi input, partial amount, change present", + fundDesc: &tapsend.FundingDescriptor{ + AssetSpecifier: asset.NewSpecifierFromGroupKey( + *groupPubKey, + ), + Amount: mintAmount * 3, + }, + vPkt: &tappsbt.VPacket{ + ChainParams: testParams, + Outputs: []*tappsbt.VOutput{{ + Type: tappsbt.TypeSplitRoot, + Amount: 0, + ScriptKey: asset.NUMSScriptKey, + Interactive: false, + }, { + Amount: mintAmount * 3, + ScriptKey: scriptKey, + Interactive: false, + AnchorOutputIndex: 1, + }}, + }, + inputProofs: []*proof.Proof{&groupProof1, &groupProof2}, + selectedCommitments: []*AnchoredCommitment{{ + AnchorPoint: groupProof1.OutPoint(), + InternalKey: grpInternalKey1, + Commitment: grpInputCommitment1, + Asset: &groupInputAsset1, + }, { + AnchorPoint: groupProof2.OutPoint(), + InternalKey: grpInternalKey2, + Commitment: grpInputCommitment2, + Asset: &groupInputAsset2, + }}, + keysDerived: 3, + expectedInputCommitments: tappsbt.InputCommitments{ + grpInputPrevID1: grpInputCommitment1, + grpInputPrevID2: grpInputCommitment2, + }, + // We test that we have two virtual packets, both with + // one input and two outputs. The first vPacket will be + // a full-value spend, so the change should be the NUMS + // key. The second vPacket is a partial spend, so there + // should be change and a key derived for it. The + // vOutputs across the two vPackets should each go to + // the same anchor output. And the same anchor output + // key should be derived for the same output indexes. + expectedOutputs: func(t *testing.T, + r *tapgarden.MockKeyRing) [][]*tappsbt.VOutput { + + pkt0Outputs := []*tappsbt.VOutput{{ + Amount: mintAmount, + Type: tappsbt.TypeSplitRoot, + ScriptKey: r.ScriptKeyAt(t, 0), + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 1, + }} + pkt1Outputs := []*tappsbt.VOutput{{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + ScriptKey: asset.NUMSScriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 1, + ), + AnchorOutputIndex: 0, + }, { + Amount: mintAmount * 2, + Type: tappsbt.TypeSimple, + ScriptKey: scriptKey, + AnchorOutputInternalKey: r.PubKeyAt( + t, 2, + ), + AnchorOutputIndex: 1, + }} + + return [][]*tappsbt.VOutput{ + pkt0Outputs, pkt1Outputs, + } + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(tt *testing.T) { exporter := &mockExporter{ - singleProof: inputProof, + proofs: tc.inputProofs, } addrBook := &mockAddrBook{} keyRing := tapgarden.NewMockKeyRing() diff --git a/tapfreighter/log.go b/tapfreighter/log.go index db2ae16b5..7429f51de 100644 --- a/tapfreighter/log.go +++ b/tapfreighter/log.go @@ -2,6 +2,7 @@ package tapfreighter import ( "github.com/btcsuite/btclog" + "github.com/davecgh/go-spew/spew" ) // Subsystem defines the logging code for this subsystem. @@ -24,3 +25,10 @@ func DisableLog() { func UseLogger(logger btclog.Logger) { log = logger } + +// limitSpewer is a spew.ConfigState that limits the depth of the output +// to 4 levels, so it can safely be used for things that contain an MS-SMT tree. +var limitSpewer = &spew.ConfigState{ + Indent: " ", + MaxDepth: 7, +} From b2a58bb8fb25ef6ba7ec578f1fed1e12a0424aa9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:43 +0100 Subject: [PATCH 11/13] tapchannel: make funding code order unaware With the new funding logic now potentially adding the change output first, we need to make sure the channel funding logic doesn't use hard-coded indexes. --- tapchannel/aux_funding_controller.go | 40 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/tapchannel/aux_funding_controller.go b/tapchannel/aux_funding_controller.go index c605f32e1..850f4c621 100644 --- a/tapchannel/aux_funding_controller.go +++ b/tapchannel/aux_funding_controller.go @@ -914,13 +914,16 @@ func (f *FundingController) sendInputOwnershipProofs(peerPub btcec.PublicKey, // We'll now send the signed inputs to the remote party. // // TODO(roasbeef): generalize for multi-asset - fundingAsset := vPkt.Outputs[0].Asset.Copy() + fundingOut, err := vPkt.FirstNonSplitRootOutput() + if err != nil { + return fmt.Errorf("unable to get funding asset: %w", err) + } assetOutputMsg := cmsg.NewTxAssetOutputProof( - fundingState.pid, *fundingAsset, true, + fundingState.pid, *fundingOut.Asset, true, ) log.Debugf("Sending TLV for funding asset output to remote party: %v", - limitSpewer.Sdump(fundingAsset)) + limitSpewer.Sdump(fundingOut.Asset)) err = f.cfg.PeerMessenger.SendMessage(ctx, peerPub, assetOutputMsg) if err != nil { @@ -1167,12 +1170,22 @@ func (f *FundingController) completeChannelFunding(ctx context.Context, fundingPackets := fundedVpkt.VPackets for idx := range fundingPackets { fundingPkt := fundingPackets[idx] - fundingPkt.Outputs[0].AnchorOutputBip32Derivation = nil - fundingPkt.Outputs[0].AnchorOutputTaprootBip32Derivation = nil + + // The funding output is the first non-split output (the split + // output is only present if there is change from the channel + // funding). + fundingOut, err := fundingPkt.FirstNonSplitRootOutput() + if err != nil { + return nil, fmt.Errorf("unable to find funding output "+ + "in funded packet: %w", err) + } + + fundingOut.AnchorOutputBip32Derivation = nil + fundingOut.AnchorOutputTaprootBip32Derivation = nil fundingInternalKeyDesc := keychain.KeyDescriptor{ PubKey: fundingInternalKey, } - fundingPkt.Outputs[0].SetAnchorInternalKey( + fundingOut.SetAnchorInternalKey( fundingInternalKeyDesc, f.cfg.ChainParams.HDCoinType, ) } @@ -1617,11 +1630,16 @@ func (f *FundingController) processFundingReq(fundingFlows fundingFlowIndex, // we can derive the tapscript root that'll be used alongside the // internal key (which we'll only learn from lnd later as we finalize // the funding PSBT). - fundingAssets := fn.Map( - fundingVpkt.VPackets, func(pkt *tappsbt.VPacket) *asset.Asset { - return pkt.Outputs[0].Asset.Copy() - }, - ) + fundingAssets := make([]*asset.Asset, 0, len(fundingVpkt.VPackets)) + for _, pkt := range fundingVpkt.VPackets { + fundingOut, err := pkt.FirstNonSplitRootOutput() + if err != nil { + return fmt.Errorf("unable to find funding output in "+ + "packet: %w", err) + } + + fundingAssets = append(fundingAssets, fundingOut.Asset.Copy()) + } fundingCommitVersion, err := tappsbt.CommitmentVersion( fundingVpkt.VPackets[0].Version, ) From b12cc95a999e0b0ac71761069b7eef8f2acb5d7e Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:44 +0100 Subject: [PATCH 12/13] itest: fix output order --- itest/assertions.go | 30 +++++++++++++++++++++++++ itest/burn_test.go | 53 ++++++++++++++++----------------------------- itest/psbt_test.go | 39 +++++++++++++++++++++------------ 3 files changed, 74 insertions(+), 48 deletions(-) diff --git a/itest/assertions.go b/itest/assertions.go index d6e3f1163..bf7edef14 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -1416,6 +1416,36 @@ func AssertNumGroups(t *testing.T, client taprpc.TaprootAssetsClient, require.Equal(t, num, NumGroups(t, client)) } +// AssertNumBurns makes sure a given number of burns exists, then returns them. +func AssertNumBurns(t *testing.T, client taprpc.TaprootAssetsClient, + num int, req *taprpc.ListBurnsRequest) []*taprpc.AssetBurn { + + ctxb := context.Background() + if req == nil { + req = &taprpc.ListBurnsRequest{} + } + + var result []*taprpc.AssetBurn + err := wait.NoError(func() error { + burns, err := client.ListBurns(ctxb, req) + if err != nil { + return err + } + + if len(burns.Burns) != num { + return fmt.Errorf("wanted %d burns, got %d", num, + len(burns.Burns)) + } + + result = burns.Burns + + return nil + }, defaultTimeout) + require.NoError(t, err) + + return result +} + // NumGroups returns the current number of asset groups present. func NumGroups(t *testing.T, client taprpc.TaprootAssetsClient) int { ctxb := context.Background() diff --git a/itest/burn_test.go b/itest/burn_test.go index d4f597d05..ec65142a5 100644 --- a/itest/burn_test.go +++ b/itest/burn_test.go @@ -64,21 +64,21 @@ func testBurnAssets(t *harnessTest) { // - 800 units to scriptKey4 // anchor index 3 (automatic change output): // - 300 units to new script key - outputAmounts := []uint64{1100, 1200, 1600, 800, 300} + outputAmounts := []uint64{300, 1100, 1200, 1600, 800} vPkt := tappsbt.ForInteractiveSend( - simpleAssetID, outputAmounts[0], scriptKey1, 0, 0, 0, + simpleAssetID, outputAmounts[1], scriptKey1, 0, 0, 0, anchorInternalKeyDesc1, asset.V0, chainParams, ) tappsbt.AddOutput( - vPkt, outputAmounts[1], scriptKey2, 0, anchorInternalKeyDesc1, + vPkt, outputAmounts[2], scriptKey2, 0, anchorInternalKeyDesc1, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[2], scriptKey3, 1, anchorInternalKeyDesc2, + vPkt, outputAmounts[3], scriptKey3, 1, anchorInternalKeyDesc2, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[3], scriptKey4, 2, anchorInternalKeyDesc3, + vPkt, outputAmounts[4], scriptKey4, 2, anchorInternalKeyDesc3, asset.V0, ) @@ -120,7 +120,7 @@ func testBurnAssets(t *harnessTest) { Asset: &taprpc.BurnAssetRequest_AssetId{ AssetId: simpleAssetID[:], }, - AmountToBurn: outputAmounts[2], + AmountToBurn: outputAmounts[3], ConfirmationText: taprootassets.AssetBurnConfirmationText, }) require.ErrorContains( @@ -152,7 +152,7 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId, - []uint64{burnAmt, outputAmounts[2] - burnAmt}, 1, 2, 2, true, + []uint64{outputAmounts[3] - burnAmt, burnAmt}, 1, 2, 2, true, ) // We'll now assert that the burned asset has the correct state. @@ -175,11 +175,8 @@ func testBurnAssets(t *harnessTest) { t.t, t.tapd, simpleAssetGen.AssetId, simpleAsset.Amount-burnAmt, ) - burns, err := t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{}) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) - burn := burns.Burns[0] + burns := AssertNumBurns(t.t, t.tapd, 1, nil) + burn := burns[0] require.Equal(t.t, uint64(burnAmt), burn.Amount) require.Equal(t.t, burnResp.BurnTransfer.AnchorTxHash, burn.AnchorTxid) require.Equal(t.t, burn.AssetId, simpleAssetID[:]) @@ -188,7 +185,7 @@ func testBurnAssets(t *harnessTest) { // The burned asset should be pruned from the tree when we next spend // the anchor output it was in (together with the change). So let's test // that we can successfully spend the change output. - secondSendAmt := outputAmounts[2] - burnAmt + secondSendAmt := outputAmounts[3] - burnAmt fullSendAddr, stream := NewAddrWithEventStream( t.t, t.tapd, &taprpc.NewAddrRequest{ AssetId: simpleAssetGen.AssetId, @@ -232,7 +229,7 @@ func testBurnAssets(t *harnessTest) { // two largest inputs we have, the one over 1500 we sent above and the // 1200 from the initial fan out transfer. const changeAmt = 300 - multiBurnAmt := outputAmounts[1] + secondSendAmt - changeAmt + multiBurnAmt := outputAmounts[2] + secondSendAmt - changeAmt burnResp, err = t.tapd.BurnAsset(ctxt, &taprpc.BurnAssetRequest{ Asset: &taprpc.BurnAssetRequest_AssetId{ AssetId: simpleAssetGen.AssetId, @@ -250,7 +247,7 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleAssetGen.AssetId, - []uint64{multiBurnAmt, changeAmt}, 4, 5, 2, true, + []uint64{changeAmt, multiBurnAmt}, 4, 5, 2, true, ) // Our final asset balance should be reduced by both successful burn @@ -290,18 +287,15 @@ func testBurnAssets(t *harnessTest) { AssertAssetOutboundTransferWithOutputs( t.t, minerClient, t.tapd, burnResp.BurnTransfer, simpleGroupGen.AssetId, - []uint64{burnAmt, simpleGroup.Amount - burnAmt}, 5, 6, 2, true, + []uint64{simpleGroup.Amount - burnAmt, burnAmt}, 5, 6, 2, true, ) AssertBalanceByID( t.t, t.tapd, simpleGroupGen.AssetId, simpleGroup.Amount-burnAmt, ) - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{}) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 4) + burns = AssertNumBurns(t.t, t.tapd, 4, nil) var groupBurn *taprpc.AssetBurn - for _, b := range burns.Burns { + for _, b := range burns { if bytes.Equal(b.AssetId, simpleGroupGen.AssetId) { groupBurn = b } @@ -356,30 +350,21 @@ func testBurnAssets(t *harnessTest) { // Fetch the burns related to the simple asset id, which should have a // total of 2 burns (tc1 & tc4). - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 2, &taprpc.ListBurnsRequest{ AssetId: simpleAssetGen.AssetId, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 2) // Fetch the burns related to the group key of the grouped asset in tc5. // There should be 1 burn. - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 1, &taprpc.ListBurnsRequest{ TweakedGroupKey: simpleGroup.AssetGroup.TweakedGroupKey, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) // Fetch the burns associated with the txhash of the burn in tc5. There // should be 1 burn returned. - burns, err = t.tapd.ListBurns(ctxt, &taprpc.ListBurnsRequest{ + AssertNumBurns(t.t, t.tapd, 1, &taprpc.ListBurnsRequest{ AnchorTxid: groupBurnTxHash, }) - require.NoError(t.t, err) - - require.Len(t.t, burns.Burns, 1) } // testBurnGroupedAssets tests that some amount of an asset from an asset group @@ -464,7 +449,7 @@ func testBurnGroupedAssets(t *harnessTest) { // Assert that the asset burn transfer occurred correctly. AssertAssetOutboundTransferWithOutputs( t.t, miner, t.tapd, burnResp.BurnTransfer, - burnAssetID, []uint64{burnAmt, postBurnAmt}, 0, 1, 2, true, + burnAssetID, []uint64{postBurnAmt, burnAmt}, 0, 1, 2, true, ) // Ensure that the burnt asset has the correct state. diff --git a/itest/psbt_test.go b/itest/psbt_test.go index 00512b8b4..7085e0be7 100644 --- a/itest/psbt_test.go +++ b/itest/psbt_test.go @@ -871,7 +871,7 @@ func runPsbtInteractiveSplitSendTest(ctxt context.Context, t *harnessTest, ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, sender, sendResp, genInfo.AssetId, - []uint64{sendAmt, changeAmt}, i/2, (i/2)+1, + []uint64{changeAmt, sendAmt}, i/2, (i/2)+1, numOutputs, ) @@ -1015,6 +1015,17 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { vPkt.Outputs[1].AltLeaves = nil fundResp := fundPacket(t, sender, vPkt) + + // The funding added a change output at the first index. So all the + // indexes will be shifted below. The output with index 1 becomes index + // 2 (where we cleared the alt leaves). + fundedvPkt, err := tappsbt.Decode(fundResp.FundedPsbt) + require.NoError(t.t, err) + require.Len(t.t, fundedvPkt.Outputs, 3) + require.Nil(t.t, fundedvPkt.Outputs[0].AltLeaves) + require.NotNil(t.t, fundedvPkt.Outputs[1].AltLeaves) + require.Nil(t.t, fundedvPkt.Outputs[2].AltLeaves) + signActiveResp, err := sender.SignVirtualPsbt( ctxt, &wrpc.SignVirtualPsbtRequest{ FundedPsbt: fundResp.FundedPsbt, @@ -1037,7 +1048,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { require.NoError(t.t, err) signedvPktCopy := signedvPkt.Copy() - require.NoError(t.t, signedvPkt.Outputs[1].SetAltLeaves(altLeaves3)) + require.NoError(t.t, signedvPkt.Outputs[2].SetAltLeaves(altLeaves3)) signedvPktBytes, err := tappsbt.Encode(signedvPkt) require.NoError(t.t, err) @@ -1075,7 +1086,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { // Now, let's set non-conflicting altLeaves for the second vOutput, and // complete the transfer via the PSBT flow. This should succeed. - require.NoError(t.t, signedvPktCopy.Outputs[1].SetAltLeaves(altLeaves2)) + require.NoError(t.t, signedvPktCopy.Outputs[2].SetAltLeaves(altLeaves2)) signedvPktBytes, err = tappsbt.Encode(signedvPktCopy) require.NoError(t.t, err) @@ -1105,7 +1116,7 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { ) expectedAmounts := []uint64{ - partialAmt, partialAmt * 2, partialAmt, + partialAmt, partialAmt, partialAmt * 2, } ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, sender, publishResp, @@ -1136,8 +1147,8 @@ func testPsbtInteractiveAltLeafAnchoring(t *harnessTest) { string(receiverScriptKey2Bytes): allAltLeaves, } - for _, asset := range receiverAssets.Assets { - AssertProofAltLeaves(t.t, receiver, asset, leafMap) + for _, receiverAsset := range receiverAssets.Assets { + AssertProofAltLeaves(t.t, receiver, receiverAsset, leafMap) } } @@ -1215,7 +1226,7 @@ func testPsbtInteractiveTapscriptSibling(t *harnessTest) { ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, alice, sendResp, - genInfo.AssetId, []uint64{sendAmt, changeAmt}, 0, 1, 2, + genInfo.AssetId, []uint64{changeAmt, sendAmt}, 0, 1, 2, ) // This is an interactive transfer, so we do need to manually send the @@ -1314,9 +1325,9 @@ func testPsbtMultiSend(t *harnessTest) { senderScriptKey2, _ := DeriveKeys(t.t, sender) // We create the output at anchor index 0 for the first address. - outputAmounts := []uint64{1200, 1300, 1400, 800, 300} + outputAmounts := []uint64{300, 1200, 1300, 1400, 800} vPkt := tappsbt.ForInteractiveSend( - id, outputAmounts[0], receiverScriptKey1, 0, 0, 0, + id, outputAmounts[1], receiverScriptKey1, 0, 0, 0, receiverAnchorIntKeyDesc1, asset.V0, chainParams, ) @@ -1325,15 +1336,15 @@ func testPsbtMultiSend(t *harnessTest) { // still leave 300 units as change which we expect to end up at anchor // index 3. tappsbt.AddOutput( - vPkt, outputAmounts[1], receiverScriptKey2, 1, + vPkt, outputAmounts[2], receiverScriptKey2, 1, receiverAnchorIntKeyDesc2, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[2], senderScriptKey1, 2, + vPkt, outputAmounts[3], senderScriptKey1, 2, senderAnchorIntKeyDesc1, asset.V0, ) tappsbt.AddOutput( - vPkt, outputAmounts[3], senderScriptKey2, 2, + vPkt, outputAmounts[4], senderScriptKey2, 2, senderAnchorIntKeyDesc1, asset.V0, ) @@ -1426,7 +1437,7 @@ func testPsbtMultiSend(t *harnessTest) { // that shared the anchor output and the other one is treated as a // passive asset. sendAssetAndAssert( - ctxt, t, t.tapd, secondTapd, outputAmounts[2], 0, + ctxt, t, t.tapd, secondTapd, outputAmounts[3], 0, genInfo, rpcAssets[0], 2, 3, 2, ) } @@ -1632,7 +1643,7 @@ func testMultiInputPsbtSingleAssetID(t *harnessTest) { ConfirmAndAssertOutboundTransferWithOutputs( t.t, t.lndHarness.Miner().Client, secondaryTapd, sendResp, genInfo.AssetId, - []uint64{sendAmt, changeAmt}, currentTransferIdx, numTransfers, + []uint64{changeAmt, sendAmt}, currentTransferIdx, numTransfers, numOutputs, ) From bb884178693876c09c8077dd25d28471fd6207fc Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 4 Mar 2025 16:52:46 +0100 Subject: [PATCH 13/13] tapsend: remove now unused functions We are now validating the input amount and types when creating the allocations and no longer need this function. --- tapsend/send.go | 102 -------------------- tapsend/send_test.go | 218 ------------------------------------------- 2 files changed, 320 deletions(-) diff --git a/tapsend/send.go b/tapsend/send.go index 8ed294303..794944fb7 100644 --- a/tapsend/send.go +++ b/tapsend/send.go @@ -277,108 +277,6 @@ func DescribeAddrs(addrs []*address.Tap) (*FundingDescriptor, error) { return desc, nil } -// AssetFromTapCommitment uses a script key to extract an asset from a given -// Taproot Asset commitment. -func AssetFromTapCommitment(tapCommitment *commitment.TapCommitment, - specifier asset.Specifier, - inputScriptKey btcec.PublicKey) (*asset.Asset, error) { - - // The top-level Taproot Asset tree must have a non-empty asset tree at - // the leaf specified by the funding descriptor's asset (group) specific - // commitment locator. - tapKey := asset.TapCommitmentKey(specifier) - assetCommitments := tapCommitment.Commitments() - assetCommitment, ok := assetCommitments[tapKey] - if !ok { - return nil, fmt.Errorf("input commitment does "+ - "not contain asset=%s: %w", &specifier, - ErrMissingInputAsset) - } - - // Determine whether issuance is disabled for the asset. - issuanceDisabled := !specifier.HasGroupPubKey() - - assetId, err := specifier.UnwrapIdOrErr() - if err != nil { - return nil, fmt.Errorf("asset from tap commitment: %w", err) - } - - // The asset tree must have a non-empty Asset at the location - // specified by the sender's script key. - assetCommitmentKey := asset.AssetCommitmentKey( - assetId, &inputScriptKey, issuanceDisabled, - ) - inputAsset, ok := assetCommitment.Asset(assetCommitmentKey) - if !ok { - return nil, fmt.Errorf("input commitment does not "+ - "contain leaf with script_key=%x: %w", - inputScriptKey.SerializeCompressed(), - ErrMissingInputAsset) - } - - return inputAsset, nil -} - -// ValidateInputs validates a set of inputs against a funding request. It -// returns true if the inputs would be spent fully, otherwise false. -func ValidateInputs(inputCommitments tappsbt.InputCommitments, - expectedAssetType asset.Type, specifier asset.Specifier, - outputAmount uint64) (bool, error) { - - // Extract the input assets from the input commitments. - inputAssets := make([]*asset.Asset, 0, len(inputCommitments)) - for prevID := range inputCommitments { - tapCommitment := inputCommitments[prevID] - senderScriptKey, err := prevID.ScriptKey.ToPubKey() - if err != nil { - return false, fmt.Errorf("unable to parse sender "+ - "script key: %v", err) - } - - // Gain the asset that we'll use as an input and in the process - // validate the selected input and commitment. - inputAsset, err := AssetFromTapCommitment( - tapCommitment, specifier, *senderScriptKey, - ) - if err != nil { - return false, err - } - - // Ensure input asset has the expected type. - if inputAsset.Type != expectedAssetType { - return false, fmt.Errorf("unexpected input asset type") - } - - inputAssets = append(inputAssets, inputAsset) - } - - // Validate total amount of input assets and determine full value spend - // status. - var isFullValueSpend bool - switch expectedAssetType { - case asset.Normal: - // Sum the total amount of the input assets. - var totalInputsAmount uint64 - for _, inputAsset := range inputAssets { - totalInputsAmount += inputAsset.Amount - } - - // Ensure that the input assets are sufficient to cover the amount - // being sent. - if totalInputsAmount < outputAmount { - return false, ErrInsufficientInputAssets - } - - // Check if the input assets are fully spent. - isFullValueSpend = totalInputsAmount == outputAmount - - case asset.Collectible: - isFullValueSpend = true - } - - return isFullValueSpend, nil -} - // ValidateCommitmentKeysUnique makes sure the outputs of a set of virtual // packets don't lead to collisions in and of the trees (e.g. two asset outputs // with the same asset ID and script key in the same anchor output) or with diff --git a/tapsend/send_test.go b/tapsend/send_test.go index bbcaa4014..ad5501f33 100644 --- a/tapsend/send_test.go +++ b/tapsend/send_test.go @@ -1873,224 +1873,6 @@ func TestProofVerifyFullValueSplit(t *testing.T) { require.NoError(t, err) } -// TestAddressValidInput tests edge cases around validating inputs for asset -// transfers with isValidInput. -func TestAddressValidInput(t *testing.T) { - t.Parallel() - - for _, testCase := range addressValidInputTestCases { - success := t.Run(testCase.name, func(t *testing.T) { - inputAsset, checkedInputAsset, err := testCase.f(t) - require.ErrorIs(t, err, testCase.err) - if testCase.err == nil { - require.True(t, inputAsset.DeepEqual( - checkedInputAsset, - )) - } - }) - if !success { - return - } - } -} - -func addrToFundDesc(addr address.Tap) *tapsend.FundingDescriptor { - assetSpecifier := asset.NewSpecifierOptionalGroupPubKey( - addr.AssetID, addr.GroupKey, - ) - - return &tapsend.FundingDescriptor{ - AssetSpecifier: assetSpecifier, - Amount: addr.Amount, - } -} - -type addressValidInputTestCase struct { - name string - f func(t *testing.T) (*asset.Asset, *asset.Asset, error) - err error -} - -var addressValidInputTestCases = []addressValidInputTestCase{{ - name: "valid normal", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: nil, -}, { - name: "valid collectible with group key", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1CollectGroup) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1CollectGroupTapTree, - fundDesc.AssetSpecifier, state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - inputCommitment := &state.asset1CollectGroupTapTree - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1CollectGroupPrevID: inputCommitment, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1CollectGroup, inputAsset, nil - }, - err: nil, -}, { - name: "valid asset split", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset2TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset2PrevID: &state.asset2TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.False(t, fullValue) - - return &state.asset2, inputAsset, nil - }, - err: nil, -}, { - name: "normal with insufficient amount", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address2) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrInsufficientInputAssets, -}, { - name: "collectible with missing input asset", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - fundDesc := addrToFundDesc(state.address1CollectGroup) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.spenderScriptKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.False(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrMissingInputAsset, -}, { - name: "normal with bad sender script key", - f: func(t *testing.T) (*asset.Asset, *asset.Asset, error) { - state := initSpendScenario(t) - - address1testnet, err := address.New( - address.V0, state.genesis1, nil, nil, - state.receiverPubKey, state.receiverPubKey, - state.normalAmt1, nil, &address.TestNet3Tap, - address.RandProofCourierAddr(t), - ) - require.NoError(t, err) - - fundDesc := addrToFundDesc(*address1testnet) - - inputAsset, err := tapsend.AssetFromTapCommitment( - &state.asset1TapTree, fundDesc.AssetSpecifier, - state.receiverPubKey, - ) - if err != nil { - return nil, nil, err - } - - fullValue, err := tapsend.ValidateInputs( - tappsbt.InputCommitments{ - state.asset1PrevID: &state.asset1TapTree, - }, inputAsset.Type, fundDesc.AssetSpecifier, - fundDesc.Amount, - ) - if err != nil { - return nil, nil, err - } - require.True(t, fullValue) - - return &state.asset1, inputAsset, nil - }, - err: tapsend.ErrMissingInputAsset, -}} - // TestPayToAddrScript tests edge cases around creating a P2TR script with // PayToAddrScript. func TestPayToAddrScript(t *testing.T) {