@@ -13,10 +13,10 @@ import (
1313 "github.com/lightninglabs/taproot-assets/address"
1414 "github.com/lightninglabs/taproot-assets/asset"
1515 "github.com/lightninglabs/taproot-assets/commitment"
16- "github.com/lightninglabs/taproot-assets/fn"
1716 "github.com/lightninglabs/taproot-assets/proof"
1817 "github.com/lightninglabs/taproot-assets/tappsbt"
1918 "github.com/lightninglabs/taproot-assets/tapsend"
19+ "golang.org/x/exp/maps"
2020)
2121
2222// createFundedPacketWithInputs funds a set of virtual transaction with the
@@ -25,80 +25,139 @@ import (
2525// single asset ID/tranche or group key with multiple tranches).
2626func createFundedPacketWithInputs (ctx context.Context , exporter proof.Exporter ,
2727 keyRing KeyRing , addrBook AddrBook , fundDesc * tapsend.FundingDescriptor ,
28- vPkt * tappsbt.VPacket ,
28+ vPktTemplate * tappsbt.VPacket ,
2929 selectedCommitments []* AnchoredCommitment ) (* FundedVPacket , error ) {
3030
31- if vPkt .ChainParams == nil {
31+ if vPktTemplate .ChainParams == nil {
3232 return nil , errors .New ("chain params not set in virtual packet" )
3333 }
34+ chainParams := vPktTemplate .ChainParams
3435
3536 log .Infof ("Selected %v asset inputs for send of %d to %s" ,
3637 len (selectedCommitments ), fundDesc .Amount ,
3738 & fundDesc .AssetSpecifier )
3839
39- assetType := selectedCommitments [0 ].Asset .Type
40-
41- totalInputAmt := uint64 (0 )
40+ var inputSum uint64
41+ inputProofs := make (
42+ map [asset.PrevID ]* proof.Proof , len (selectedCommitments ),
43+ )
44+ selectedCommitmentsByPrevID := make (
45+ map [asset.PrevID ]* AnchoredCommitment , len (selectedCommitments ),
46+ )
4247 for _ , anchorAsset := range selectedCommitments {
43- // We only use the sum of all assets of the same TAP commitment
44- // key to avoid counting passive assets as well. We'll filter
45- // out the passive assets from the selected commitments in a
46- // later step .
48+ // We only use the inputs of assets of the same TAP commitment
49+ // as we want to fund for. These are the active assets that
50+ // we're going to distribute. All other assets are passive and
51+ // will be detected and added later .
4752 if anchorAsset .Asset .TapCommitmentKey () !=
4853 fundDesc .TapCommitmentKey () {
4954
5055 continue
5156 }
5257
53- totalInputAmt += anchorAsset .Asset .Amount
58+ // We'll also include an inclusion proof for the input asset in
59+ // the virtual transaction. With that a signer can verify that
60+ // the asset was actually committed to in the anchor output.
61+ inputProof , err := fetchInputProof (
62+ ctx , exporter , anchorAsset .Asset ,
63+ anchorAsset .AnchorPoint ,
64+ )
65+ if err != nil {
66+ return nil , fmt .Errorf ("error fetching input proof: %w" ,
67+ err )
68+ }
69+
70+ inputSum += anchorAsset .Asset .Amount
71+ inputProofs [anchorAsset .PrevID ()] = inputProof
72+ selectedCommitmentsByPrevID [anchorAsset .PrevID ()] = anchorAsset
5473 }
5574
56- inputCommitments , err := setVPacketInputs (
57- ctx , exporter , selectedCommitments , vPkt ,
58- )
75+ // We try to identify and annotate any script keys in the template that
76+ // might be ours.
77+ err := annotateLocalScriptKeys ( ctx , vPktTemplate , addrBook )
5978 if err != nil {
60- return nil , err
79+ return nil , fmt .Errorf ("error annotating local script " +
80+ "keys: %w" , err )
6181 }
6282
63- fullValue , err := tapsend .ValidateInputs (
64- inputCommitments , assetType , fundDesc .AssetSpecifier ,
65- fundDesc .Amount ,
83+ allocations , interactive , err := tapsend .AllocationsFromTemplate (
84+ vPktTemplate , inputSum ,
6685 )
6786 if err != nil {
68- return nil , err
87+ return nil , fmt . Errorf ( "error extracting allocations: %w" , err )
6988 }
7089
71- // Make sure we'll recognize local script keys in the virtual packet
72- // later on in the process by annotating them with the full descriptor
73- // information.
74- if err := annotateLocalScriptKeys (ctx , vPkt , addrBook ); err != nil {
75- return nil , err
90+ allPackets , err := tapsend .DistributeCoins (
91+ maps .Values (inputProofs ), allocations , chainParams , interactive ,
92+ vPktTemplate .Version ,
93+ )
94+ if err != nil {
95+ return nil , fmt .Errorf ("unable to distribute coins: %w" , err )
7696 }
7797
78- // If we don't spend the full value, we need to create a change output.
79- changeAmount := totalInputAmt - fundDesc .Amount
80- err = createChangeOutput (ctx , vPkt , keyRing , fullValue , changeAmount )
81- if err != nil {
82- return nil , err
98+ // Add all the input information to the virtual packets and also make
99+ // sure we have proper change output keys for non-zero change outputs.
100+ for _ , vPkt := range allPackets {
101+ for idx := range vPkt .Inputs {
102+ prevID := vPkt .Inputs [idx ].PrevID
103+ assetInput , ok := selectedCommitmentsByPrevID [prevID ]
104+ if ! ok {
105+ return nil , fmt .Errorf ("input commitment not " +
106+ "found for prevID %v" , prevID )
107+ }
108+
109+ inputProof , ok := inputProofs [prevID ]
110+ if ! ok {
111+ return nil , fmt .Errorf ("input proof not found " +
112+ "for prevID %v" , prevID )
113+ }
114+
115+ err = createAndSetInput (
116+ vPkt , idx , assetInput , inputProof ,
117+ )
118+ if err != nil {
119+ return nil , fmt .Errorf ("unable to create and " +
120+ "set input: %w" , err )
121+ }
122+ }
123+
124+ err = deriveChangeOutputKey (ctx , vPkt , keyRing )
125+ if err != nil {
126+ return nil , fmt .Errorf ("unable to derive change " +
127+ "output key: %w" , err )
128+ }
83129 }
84130
85131 // Before we can prepare output assets for our send, we need to generate
86132 // a new internal key for the anchor outputs. We assume any output that
87133 // hasn't got an internal key set is going to a local anchor, and we
88134 // provide the internal key for that.
89- packets := []* tappsbt.VPacket {vPkt }
90- err = generateOutputAnchorInternalKeys (ctx , packets , keyRing )
135+ err = generateOutputAnchorInternalKeys (ctx , allPackets , keyRing )
91136 if err != nil {
92137 return nil , fmt .Errorf ("unable to generate output anchor " +
93138 "internal keys: %w" , err )
94139 }
95140
96- if err := tapsend .PrepareOutputAssets (ctx , vPkt ); err != nil {
97- return nil , fmt .Errorf ("unable to prepare outputs: %w" , err )
141+ for _ , vPkt := range allPackets {
142+ if err := tapsend .PrepareOutputAssets (ctx , vPkt ); err != nil {
143+ log .Errorf ("Error preparing output assets: %v, " +
144+ "packets: %v" , err , limitSpewer .Sdump (vPkt ))
145+ return nil , fmt .Errorf ("unable to prepare outputs: %w" ,
146+ err )
147+ }
148+ }
149+
150+ // Extract just the TAP commitments by input from the selected anchored
151+ // commitments.
152+ inputCommitments := make (
153+ tappsbt.InputCommitments , len (selectedCommitmentsByPrevID ),
154+ )
155+ for prevID , anchorAsset := range selectedCommitmentsByPrevID {
156+ inputCommitments [prevID ] = anchorAsset .Commitment
98157 }
99158
100159 return & FundedVPacket {
101- VPackets : packets ,
160+ VPackets : allPackets ,
102161 InputCommitments : inputCommitments ,
103162 }, nil
104163}
@@ -138,56 +197,35 @@ func annotateLocalScriptKeys(ctx context.Context, vPkt *tappsbt.VPacket,
138197 return nil
139198}
140199
141- // createChangeOutput creates a change output for the given virtual packet if
142- // it isn't fully spent.
143- func createChangeOutput (ctx context.Context , vPkt * tappsbt.VPacket ,
144- keyRing KeyRing , fullValue bool , changeAmount uint64 ) error {
145-
146- // If we're spending the full value, we don't need a change output. We
147- // currently assume that if it's a full-value non-interactive spend that
148- // the packet was created with the correct function in the tappsbt
149- // packet that adds the NUMS script key output for the tombstone. If
150- // the user doesn't set that, then an error will be returned from the
151- // tapsend.PrepareOutputAssets function. But we should probably change
152- // that and allow the user to specify a minimum packet template and add
153- // whatever else is needed to it automatically.
154- if fullValue {
200+ // deriveChangeOutputKey makes sure the change output has a proper key that goes
201+ // back to the local node, assuming there is a change output and it isn't a
202+ // zero-value tombstone.
203+ func deriveChangeOutputKey (ctx context.Context , vPkt * tappsbt.VPacket ,
204+ keyRing KeyRing ) error {
205+
206+ // If we don't have a split output then there's no change.
207+ if ! vPkt .HasSplitRootOutput () {
155208 return nil
156209 }
157210
158- // We expect some change back, or have passive assets to commit to, so
159- // let's make sure we create a transfer output.
160211 changeOut , err := vPkt .SplitRootOutput ()
161212 if err != nil {
162- lastOut := vPkt .Outputs [len (vPkt .Outputs )- 1 ]
163- splitOutIndex := lastOut .AnchorOutputIndex + 1
164- changeOut = & tappsbt.VOutput {
165- Type : tappsbt .TypeSplitRoot ,
166- Interactive : lastOut .Interactive ,
167- AnchorOutputIndex : splitOutIndex ,
168-
169- // We want to handle deriving a real key in a
170- // generic manner, so we'll do that just below.
171- ScriptKey : asset .NUMSScriptKey ,
172- }
173-
174- vPkt .Outputs = append (vPkt .Outputs , changeOut )
213+ return err
175214 }
176215
177- // Since we know we're going to receive some change back, we
178- // need to make sure it is going to an address that we control.
179- // This should only be the case where we create the default
180- // change output with the NUMS key to avoid deriving too many
181- // keys prematurely. We don't need to derive a new key if we
182- // only have passive assets to commit to, since they all have
183- // their own script key and the output is more of a placeholder
184- // to attach the passive assets to.
216+ // Since we know we're going to receive some change back, we need to
217+ // make sure it is going to an address that we control. This should only
218+ // be the case where we create the default change output with the NUMS
219+ // key to avoid deriving too many keys prematurely. We don't need to
220+ // derive a new key if we only have passive assets to commit to, since
221+ // they all have their own script key and the output is more of a
222+ // placeholder to attach the passive assets to.
185223 unSpendable , err := changeOut .ScriptKey .IsUnSpendable ()
186224 if err != nil {
187225 return fmt .Errorf ("cannot determine if script key is " +
188226 "spendable: %w" , err )
189227 }
190- if unSpendable {
228+ if unSpendable && changeOut . Amount > 0 {
191229 changeScriptKey , err := keyRing .DeriveNextKey (
192230 ctx , asset .TaprootAssetsKeyFamily ,
193231 )
@@ -202,30 +240,6 @@ func createChangeOutput(ctx context.Context, vPkt *tappsbt.VPacket,
202240 )
203241 }
204242
205- // For existing change outputs, we'll just update the amount
206- // since we might not have known what coin would've been
207- // selected and how large the change would turn out to be.
208- changeOut .Amount = changeAmount
209-
210- // The asset version of the output should be the max of the set
211- // of input versions. We need to set this now as in
212- // PrepareOutputAssets locators are created which includes the
213- // version from the vOut. If we don't set it here, a v1 asset
214- // spent that becomes change will be a v0 if combined with such
215- // inputs.
216- //
217- // TODO(roasbeef): remove as not needed?
218- maxVersion := func (maxVersion asset.Version ,
219- vInput * tappsbt.VInput ) asset.Version {
220-
221- if vInput .Asset ().Version > maxVersion {
222- return vInput .Asset ().Version
223- }
224-
225- return maxVersion
226- }
227- changeOut .AssetVersion = fn .Reduce (vPkt .Inputs , maxVersion )
228-
229243 return nil
230244}
231245
@@ -357,53 +371,6 @@ func generateOutputAnchorInternalKeys(ctx context.Context,
357371 return nil
358372}
359373
360- // setVPacketInputs sets the inputs of the given vPkt to the given send eligible
361- // commitments. It also returns the assets that were used as inputs.
362- func setVPacketInputs (ctx context.Context , exporter proof.Exporter ,
363- eligibleCommitments []* AnchoredCommitment ,
364- vPkt * tappsbt.VPacket ) (tappsbt.InputCommitments , error ) {
365-
366- vPkt .Inputs = make ([]* tappsbt.VInput , len (eligibleCommitments ))
367- inputCommitments := make (tappsbt.InputCommitments )
368-
369- for idx := range eligibleCommitments {
370- // If the key found for the input UTXO cannot be identified as
371- // belonging to the lnd wallet, we won't be able to sign for it.
372- // This would happen if a user manually imported an asset that
373- // was issued/received for/on another node. We should probably
374- // not create asset entries for such imported assets in the
375- // first place, as we won't be able to spend it anyway. But for
376- // now we just put this check in place.
377- assetInput := eligibleCommitments [idx ]
378-
379- // We'll also include an inclusion proof for the input asset in
380- // the virtual transaction. With that a signer can verify that
381- // the asset was actually committed to in the anchor output.
382- inputProof , err := fetchInputProof (
383- ctx , exporter , assetInput .Asset , assetInput .AnchorPoint ,
384- )
385- if err != nil {
386- return nil , fmt .Errorf ("error fetching input proof: %w" ,
387- err )
388- }
389-
390- // Create the virtual packet input including the chain anchor
391- // information.
392- err = createAndSetInput (
393- vPkt , idx , assetInput , inputProof ,
394- )
395- if err != nil {
396- return nil , fmt .Errorf ("unable to create and set " +
397- "input: %w" , err )
398- }
399-
400- prevID := vPkt .Inputs [idx ].PrevID
401- inputCommitments [prevID ] = assetInput .Commitment
402- }
403-
404- return inputCommitments , nil
405- }
406-
407374// createAndSetInput creates a virtual packet input for the given asset input
408375// and sets it on the given virtual packet.
409376func createAndSetInput (vPkt * tappsbt.VPacket , idx int ,
0 commit comments