Skip to content

Commit bf69bc6

Browse files
committed
tapfreighter: make funding multi package compatible
1 parent 825add1 commit bf69bc6

File tree

3 files changed

+479
-162
lines changed

3 files changed

+479
-162
lines changed

tapfreighter/fund.go

Lines changed: 110 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -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).
2626
func 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.
409376
func createAndSetInput(vPkt *tappsbt.VPacket, idx int,

0 commit comments

Comments
 (0)