From 512028b3a654d8b273d2c750b1e6161a366345c9 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:09:46 +0200 Subject: [PATCH 1/3] assets: add asset HTLC helpers With this commit we add high level helpers along with scripts to create asset HTLCs. --- assets/htlc/script.go | 88 ++++++++ assets/htlc/swapkit.go | 460 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 548 insertions(+) create mode 100644 assets/htlc/script.go create mode 100644 assets/htlc/swapkit.go diff --git a/assets/htlc/script.go b/assets/htlc/script.go new file mode 100644 index 000000000..5a79c1a2a --- /dev/null +++ b/assets/htlc/script.go @@ -0,0 +1,88 @@ +package htlc + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/txscript" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" +) + +// GenSuccessPathScript constructs an HtlcScript for the success payment path. +func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey, + swapHash lntypes.Hash) ([]byte, error) { + + builder := txscript.NewScriptBuilder() + + builder.AddData(schnorr.SerializePubKey(receiverHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddOp(txscript.OP_SIZE) + builder.AddInt64(32) + builder.AddOp(txscript.OP_EQUALVERIFY) + builder.AddOp(txscript.OP_HASH160) + builder.AddData(input.Ripemd160H(swapHash[:])) + builder.AddOp(txscript.OP_EQUALVERIFY) + //builder.AddInt64(1) + //builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// GenTimeoutPathScript constructs an HtlcScript for the timeout payment path. +func GenTimeoutPathScript(senderHtlcKey *btcec.PublicKey, csvExpiry int64) ( + []byte, error) { + + builder := txscript.NewScriptBuilder() + builder.AddData(schnorr.SerializePubKey(senderHtlcKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(csvExpiry) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + return builder.Script() +} + +// GetOpTrueScript returns a script that always evaluates to true. +func GetOpTrueScript() ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_TRUE).Script() +} + +// CreateOpTrueLeaf creates a taproot leaf that always evaluates to true. +func CreateOpTrueLeaf() (asset.ScriptKey, txscript.TapLeaf, + *txscript.IndexedTapScriptTree, *txscript.ControlBlock, error) { + + // Create the taproot OP_TRUE script. + tapScript, err := GetOpTrueScript() + if err != nil { + return asset.ScriptKey{}, txscript.TapLeaf{}, nil, nil, err + } + + tapLeaf := txscript.NewBaseTapLeaf(tapScript) + tree := txscript.AssembleTaprootScriptTree(tapLeaf) + rootHash := tree.RootNode.TapHash() + tapKey := txscript.ComputeTaprootOutputKey(asset.NUMSPubKey, rootHash[:]) + + merkleRootHash := tree.RootNode.TapHash() + + controlBlock := &txscript.ControlBlock{ + LeafVersion: txscript.BaseLeafVersion, + InternalKey: asset.NUMSPubKey, + } + tapScriptKey := asset.ScriptKey{ + PubKey: tapKey, + TweakedScriptKey: &asset.TweakedScriptKey{ + RawKey: keychain.KeyDescriptor{ + PubKey: asset.NUMSPubKey, + }, + Tweak: merkleRootHash[:], + }, + } + if tapKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd { + + controlBlock.OutputKeyYIsOdd = true + } + + return tapScriptKey, tapLeaf, tree, controlBlock, nil +} diff --git a/assets/htlc/swapkit.go b/assets/htlc/swapkit.go new file mode 100644 index 000000000..95a288a6b --- /dev/null +++ b/assets/htlc/swapkit.go @@ -0,0 +1,460 @@ +package htlc + +import ( + "context" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tappsbt" + "github.com/lightninglabs/taproot-assets/tapscript" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" +) + +// SwapKit holds information needed to facilitate an on-chain asset to offchain +// bitcoin atomic swap. The keys within the struct are the public keys of the +// sender and receiver that will be used to create the on-chain HTLC. +type SwapKit struct { + // SenderPubKey is the public key of the sender for the joint key + // that will be used to create the HTLC. + SenderPubKey *btcec.PublicKey + + // ReceiverPubKey is the public key of the receiver that will be used to + // create the HTLC. + ReceiverPubKey *btcec.PublicKey + + // AssetID is the identifier of the asset that will be swapped. + AssetID []byte + + // Amount is the amount of the asset that will be swapped. Note that + // we use btcutil.Amount here for simplicity, but the actual amount + // is in the asset's native unit. + Amount btcutil.Amount + + // SwapHash is the hash of the preimage in the swap HTLC. + SwapHash lntypes.Hash + + // CsvExpiry is the relative timelock in blocks for the swap. + CsvExpiry uint32 + + // AddressParams is the chain parameters of the chain the deposit is + // being created on. + AddressParams *address.ChainParams +} + +// GetSuccessScript returns the success path script of the swap HTLC. +func (s *SwapKit) GetSuccessScript() ([]byte, error) { + return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash) +} + +// GetTimeoutScript returns the timeout path script of the swap HTLC. +func (s *SwapKit) GetTimeoutScript() ([]byte, error) { + return GenTimeoutPathScript(s.SenderPubKey, int64(s.CsvExpiry)) +} + +// GetAggregateKey returns the aggregate MuSig2 key used in the swap HTLC. +func (s *SwapKit) GetAggregateKey() (*btcec.PublicKey, error) { + aggregateKey, err := input.MuSig2CombineKeys( + input.MuSig2Version100RC2, + []*btcec.PublicKey{ + s.SenderPubKey, s.ReceiverPubKey, + }, + true, + &input.MuSig2Tweaks{}, + ) + if err != nil { + return nil, err + } + + return aggregateKey.PreTweakedKey, nil +} + +// GetTimeOutLeaf returns the timeout leaf of the swap. +func (s *SwapKit) GetTimeOutLeaf() (txscript.TapLeaf, error) { + timeoutScript, err := s.GetTimeoutScript() + if err != nil { + return txscript.TapLeaf{}, err + } + + timeoutLeaf := txscript.NewBaseTapLeaf(timeoutScript) + + return timeoutLeaf, nil +} + +// GetSuccessLeaf returns the success leaf of the swap. +func (s *SwapKit) GetSuccessLeaf() (txscript.TapLeaf, error) { + successScript, err := s.GetSuccessScript() + if err != nil { + return txscript.TapLeaf{}, err + } + + successLeaf := txscript.NewBaseTapLeaf(successScript) + + return successLeaf, nil +} + +// GetSiblingPreimage returns the sibling preimage of the HTLC bitcoin top level +// output. +func (s *SwapKit) GetSiblingPreimage() (commitment.TapscriptPreimage, error) { + timeOutLeaf, err := s.GetTimeOutLeaf() + if err != nil { + return commitment.TapscriptPreimage{}, err + } + + successLeaf, err := s.GetSuccessLeaf() + if err != nil { + return commitment.TapscriptPreimage{}, err + } + + branch := txscript.NewTapBranch(timeOutLeaf, successLeaf) + + siblingPreimage := commitment.NewPreimageFromBranch(branch) + + return siblingPreimage, nil +} + +// CreateHtlcVpkt creates the vpacket for the HTLC. +func (s *SwapKit) CreateHtlcVpkt() (*tappsbt.VPacket, error) { + assetId := asset.ID{} + copy(assetId[:], s.AssetID) + + btcInternalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + siblingPreimage, err := s.GetSiblingPreimage() + if err != nil { + return nil, err + } + + tapScriptKey, _, _, _, err := CreateOpTrueLeaf() + if err != nil { + return nil, err + } + + pkt := &tappsbt.VPacket{ + Inputs: []*tappsbt.VInput{{ + PrevID: asset.PrevID{ + ID: assetId, + }, + }}, + Outputs: make([]*tappsbt.VOutput, 0, 2), + ChainParams: s.AddressParams, + Version: tappsbt.V1, + } + pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + AnchorOutputIndex: 0, + ScriptKey: asset.NUMSScriptKey, + }) + pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{ + // todo(sputn1ck) assetversion + AssetVersion: asset.Version(1), + Amount: uint64(s.Amount), + Interactive: true, + AnchorOutputIndex: 1, + ScriptKey: asset.NewScriptKey( + tapScriptKey.PubKey, + ), + AnchorOutputInternalKey: btcInternalKey, + AnchorOutputTapscriptSibling: &siblingPreimage, + }) + + return pkt, nil +} + +// GenTimeoutBtcControlBlock generates the control block for the timeout path of +// the swap. +func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) ( + *txscript.ControlBlock, error) { + + internalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + successLeaf, err := s.GetSuccessLeaf() + if err != nil { + return nil, err + } + + successLeafHash := successLeaf.TapHash() + + btcControlBlock := &txscript.ControlBlock{ + InternalKey: internalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: append( + successLeafHash[:], taprootAssetRoot[:]..., + ), + } + + timeoutPathScript, err := s.GetTimeoutScript() + if err != nil { + return nil, err + } + + rootHash := btcControlBlock.RootHash(timeoutPathScript) + tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash) + if tapKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd { + + btcControlBlock.OutputKeyYIsOdd = true + } + + return btcControlBlock, nil +} + +// GenSuccessBtcControlBlock generates the control block for the timeout path of +// the swap. +func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) ( + *txscript.ControlBlock, error) { + + internalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + timeOutLeaf, err := s.GetTimeOutLeaf() + if err != nil { + return nil, err + } + + timeOutLeafHash := timeOutLeaf.TapHash() + + btcControlBlock := &txscript.ControlBlock{ + InternalKey: internalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: append( + timeOutLeafHash[:], taprootAssetRoot[:]..., + ), + } + + successPathScript, err := s.GetSuccessScript() + if err != nil { + return nil, err + } + + rootHash := btcControlBlock.RootHash(successPathScript) + tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash) + if tapKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd { + + btcControlBlock.OutputKeyYIsOdd = true + } + + return btcControlBlock, nil +} + +// GenTaprootAssetRootFromProof generates the taproot asset root from the proof +// of the swap. +func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) { + assetCopy := proof.Asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets( + &version, assetCopy, + ) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot := assetCommitment.TapscriptRoot(nil) + + return taprootAssetRoot[:], nil +} + +// GetPkScriptFromAsset returns the toplevel bitcoin script with the given +// asset. +func (s *SwapKit) GetPkScriptFromAsset(asset *asset.Asset) ([]byte, error) { + assetCopy := asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets( + &version, assetCopy, + ) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + siblingPreimage, err := s.GetSiblingPreimage() + if err != nil { + return nil, err + } + + siblingHash, err := siblingPreimage.TapHash() + if err != nil { + return nil, err + } + + btcInternalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + return tapscript.PayToAddrScript( + *btcInternalKey, siblingHash, *assetCommitment, + ) +} + +// CreatePreimageWitness creates a preimage witness for the swap. +func (s *SwapKit) CreatePreimageWitness(ctx context.Context, + signer lndclient.SignerClient, htlcProof *proof.Proof, + sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator, + preimage lntypes.Preimage) (wire.TxWitness, error) { + + assetTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value, + } + feeTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value, + } + + //sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1 + + successScript, err := s.GetSuccessScript() + if err != nil { + return nil, err + } + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: keyLocator, + }, + SignMethod: input.TaprootScriptSpendSignMethod, + WitnessScript: successScript, + Output: assetTxOut, + InputIndex: 0, + } + sig, err := signer.SignOutputRaw( + ctx, sweepBtcPacket.UnsignedTx, + []*lndclient.SignDescriptor{ + signDesc, + }, + []*wire.TxOut{ + assetTxOut, feeTxOut, + }, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof) + if err != nil { + return nil, err + } + + successControlBlock, err := s.GenSuccessBtcControlBlock( + taprootAssetRoot, + ) + if err != nil { + return nil, err + } + + controlBlockBytes, err := successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + preimage[:], + sig[0], + successScript, + controlBlockBytes, + }, nil +} + +// CreateTimeoutWitness creates a timeout witness for the swap. +func (s *SwapKit) CreateTimeoutWitness(ctx context.Context, + signer lndclient.SignerClient, htlcProof *proof.Proof, + sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator) ( + wire.TxWitness, error) { + + assetTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value, + } + feeTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value, + } + + sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = s.CsvExpiry + + timeoutScript, err := s.GetTimeoutScript() + if err != nil { + return nil, err + } + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: keyLocator, + }, + SignMethod: input.TaprootScriptSpendSignMethod, + WitnessScript: timeoutScript, + Output: assetTxOut, + InputIndex: 0, + } + sig, err := signer.SignOutputRaw( + ctx, sweepBtcPacket.UnsignedTx, + []*lndclient.SignDescriptor{ + signDesc, + }, + []*wire.TxOut{ + assetTxOut, feeTxOut, + }, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof) + if err != nil { + return nil, err + } + + timeoutControlBlock, err := s.GenTimeoutBtcControlBlock( + taprootAssetRoot, + ) + if err != nil { + return nil, err + } + + controlBlockBytes, err := timeoutControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + sig[0], + timeoutScript, + controlBlockBytes, + }, nil +} From 7638d8bcef8efe97f76e2f5cc7be83c1b682992b Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:18:46 +0200 Subject: [PATCH 2/3] assets: add no-csv option to the asset HTLC to support package relay This commit enables package relayed HTLCs by making the CSV check in the success path optional. --- assets/htlc/script.go | 18 +++++++++++++----- assets/htlc/swapkit.go | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/assets/htlc/script.go b/assets/htlc/script.go index 5a79c1a2a..9ce7066ba 100644 --- a/assets/htlc/script.go +++ b/assets/htlc/script.go @@ -11,9 +11,12 @@ import ( "github.com/lightningnetwork/lnd/lntypes" ) -// GenSuccessPathScript constructs an HtlcScript for the success payment path. +// GenSuccessPathScript constructs a script for the success path of the HTLC +// payment. Optionally includes a CHECKSEQUENCEVERIFY (CSV) of 1 if `csv` is +// true, to prevent potential pinning attacks when the HTLC is not part of a +// package relay. func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey, - swapHash lntypes.Hash) ([]byte, error) { + swapHash lntypes.Hash, csv bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -25,8 +28,11 @@ func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey, builder.AddOp(txscript.OP_HASH160) builder.AddData(input.Ripemd160H(swapHash[:])) builder.AddOp(txscript.OP_EQUALVERIFY) - //builder.AddInt64(1) - //builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + if csv { + builder.AddInt64(1) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + } return builder.Script() } @@ -61,7 +67,9 @@ func CreateOpTrueLeaf() (asset.ScriptKey, txscript.TapLeaf, tapLeaf := txscript.NewBaseTapLeaf(tapScript) tree := txscript.AssembleTaprootScriptTree(tapLeaf) rootHash := tree.RootNode.TapHash() - tapKey := txscript.ComputeTaprootOutputKey(asset.NUMSPubKey, rootHash[:]) + tapKey := txscript.ComputeTaprootOutputKey( + asset.NUMSPubKey, rootHash[:], + ) merkleRootHash := tree.RootNode.TapHash() diff --git a/assets/htlc/swapkit.go b/assets/htlc/swapkit.go index 95a288a6b..0724d7ff9 100644 --- a/assets/htlc/swapkit.go +++ b/assets/htlc/swapkit.go @@ -50,11 +50,16 @@ type SwapKit struct { // AddressParams is the chain parameters of the chain the deposit is // being created on. AddressParams *address.ChainParams + + // CheckCSV indicates whether the success path script should include a + // CHECKSEQUENCEVERIFY check. This is used to prevent potential pinning + // attacks when the HTLC is not part of a package relay. + CheckCSV bool } // GetSuccessScript returns the success path script of the swap HTLC. func (s *SwapKit) GetSuccessScript() ([]byte, error) { - return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash) + return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash, s.CheckCSV) } // GetTimeoutScript returns the timeout path script of the swap HTLC. @@ -160,10 +165,8 @@ func (s *SwapKit) CreateHtlcVpkt() (*tappsbt.VPacket, error) { ScriptKey: asset.NUMSScriptKey, }) pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{ - // todo(sputn1ck) assetversion - AssetVersion: asset.Version(1), + AssetVersion: asset.V1, Amount: uint64(s.Amount), - Interactive: true, AnchorOutputIndex: 1, ScriptKey: asset.NewScriptKey( tapScriptKey.PubKey, @@ -196,7 +199,7 @@ func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) ( InternalKey: internalKey, LeafVersion: txscript.BaseLeafVersion, InclusionProof: append( - successLeafHash[:], taprootAssetRoot[:]..., + successLeafHash[:], taprootAssetRoot..., ), } @@ -237,7 +240,7 @@ func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) ( InternalKey: internalKey, LeafVersion: txscript.BaseLeafVersion, InclusionProof: append( - timeOutLeafHash[:], taprootAssetRoot[:]..., + timeOutLeafHash[:], taprootAssetRoot..., ), } @@ -337,7 +340,9 @@ func (s *SwapKit) CreatePreimageWitness(ctx context.Context, Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value, } - //sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1 + if s.CheckCSV { + sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1 + } successScript, err := s.GetSuccessScript() if err != nil { From 1bfa7808b69c84ef28a8af76eefe5a4021c0f990 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:32:57 +0200 Subject: [PATCH 3/3] assets: extend the tapd client and add high-level TAP helpers This commit adds additional scaffolding to our tapd client, along with new high-level helpers in the assets package, which will be used later for swaps and deposits. --- assets/client.go | 656 ++++++++++++++++++++++++++++++++++++++++++++--- assets/tapkit.go | 158 ++++++++++++ 2 files changed, 785 insertions(+), 29 deletions(-) create mode 100644 assets/tapkit.go diff --git a/assets/client.go b/assets/client.go index 88c23efe5..95052c7bc 100644 --- a/assets/client.go +++ b/assets/client.go @@ -1,6 +1,7 @@ package assets import ( + "bytes" "context" "encoding/hex" "fmt" @@ -9,19 +10,38 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/lndclient" + tap "github.com/lightninglabs/taproot-assets" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/tapcfg" + "github.com/lightninglabs/taproot-assets/tapfreighter" + "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/tapsend" + "github.com/lightninglabs/taproot-assets/universe" + "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" + "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" "gopkg.in/macaroon.v2" ) @@ -29,7 +49,7 @@ var ( // maxMsgRecvSize is the largest message our client will receive. We // set this to 200MiB atm. - maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200) + maxMsgRecvSize = grpc.MaxCallRecvMsgSize(200 * 1024 * 1024) // defaultRfqTimeout is the default timeout we wait for tapd peer to // accept RFQ. @@ -66,6 +86,7 @@ type TapdClient struct { priceoraclerpc.PriceOracleClient rfqrpc.RfqClient universerpc.UniverseClient + assetwalletrpc.AssetWalletClient cfg *TapdConfig assetNameCache map[string]string @@ -73,6 +94,43 @@ type TapdClient struct { cc *grpc.ClientConn } +func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { + // Load the specified TLS certificate and build transport credentials. + creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") + if err != nil { + return nil, err + } + + // Load the specified macaroon file. + macBytes, err := os.ReadFile(config.MacaroonPath) + if err != nil { + return nil, err + } + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(macBytes); err != nil { + return nil, err + } + + macaroon, err := macaroons.NewMacaroonCredential(mac) + if err != nil { + return nil, err + } + // Create the DialOptions with the macaroon credentials. + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + grpc.WithPerRPCCredentials(macaroon), + grpc.WithDefaultCallOptions(maxMsgRecvSize), + } + + // Dial the gRPC server. + conn, err := grpc.Dial(config.Host, opts...) + if err != nil { + return nil, err + } + + return conn, nil +} + // NewTapdClient returns a new taproot assets client. func NewTapdClient(config *TapdConfig) (*TapdClient, error) { // Create the client connection to the server. @@ -91,6 +149,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) { PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn), RfqClient: rfqrpc.NewRfqClient(conn), UniverseClient: universerpc.NewUniverseClient(conn), + AssetWalletClient: assetwalletrpc.NewAssetWalletClient(conn), } return client, nil @@ -220,13 +279,15 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, } if rfq.GetInvalidQuote() != nil { - return 0, fmt.Errorf("peer %v sent an invalid quote response %v for "+ - "asset %v", peerPubkey, rfq.GetInvalidQuote(), assetID) + return 0, fmt.Errorf("peer %v sent an invalid quote response "+ + "%v for asset %v", peerPubkey, rfq.GetInvalidQuote(), + assetID) } if rfq.GetRejectedQuote() != nil { return 0, fmt.Errorf("peer %v rejected the quote request for "+ - "asset %v, %v", peerPubkey, assetID, rfq.GetRejectedQuote()) + "asset %v, %v", peerPubkey, assetID, + rfq.GetRejectedQuote()) } acceptedRes := rfq.GetAcceptedQuote() @@ -255,6 +316,435 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( return msatAmt.ToSatoshis(), nil } +// FundAndSignVpacket funds and signs a vpacket. +func (t *TapdClient) FundAndSignVpacket(ctx context.Context, + vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error) { + + // Fund the packet. + var buf bytes.Buffer + err := vpkt.Serialize(&buf) + if err != nil { + return nil, err + } + + fundResp, err := t.FundVirtualPsbt( + ctx, &assetwalletrpc.FundVirtualPsbtRequest{ + Template: &assetwalletrpc.FundVirtualPsbtRequest_Psbt{ + Psbt: buf.Bytes(), + }, + }, + ) + if err != nil { + return nil, err + } + + // Sign the packet. + signResp, err := t.SignVirtualPsbt( + ctx, &assetwalletrpc.SignVirtualPsbtRequest{ + FundedPsbt: fundResp.FundedPsbt, + }, + ) + if err != nil { + return nil, err + } + + return tappsbt.NewFromRawBytes( + bytes.NewReader(signResp.SignedPsbt), false, + ) +} + +// addP2WPKHOutputToPsbt adds a normal bitcoin P2WPKH output to a psbt for the +// given key and amount. +func addP2WPKHOutputToPsbt(packet *psbt.Packet, keyDesc keychain.KeyDescriptor, + amount btcutil.Amount, params *chaincfg.Params) error { + + derivation, _, _ := btcwallet.Bip32DerivationFromKeyDesc( + keyDesc, params.HDCoinType, + ) + + // Convert to Bitcoin address. + pubKeyBytes := keyDesc.PubKey.SerializeCompressed() + pubKeyHash := btcutil.Hash160(pubKeyBytes) + address, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, params) + if err != nil { + return err + } + + // Generate the P2WPKH scriptPubKey. + scriptPubKey, err := txscript.PayToAddrScript(address) + if err != nil { + return err + } + + // Add the output to the packet. + packet.UnsignedTx.AddTxOut( + wire.NewTxOut(int64(amount), scriptPubKey), + ) + + packet.Outputs = append(packet.Outputs, psbt.POutput{ + Bip32Derivation: []*psbt.Bip32Derivation{ + derivation, + }, + }) + + return nil +} + +// PrepareAndCommitVirtualPsbts prepares and commits virtual psbt to a BTC +// template so that the underlying wallet can fund the transaction and add the +// necessary additional input to pay for fees as well as a change output if the +// change keydescriptor is not provided. +func (t *TapdClient) PrepareAndCommitVirtualPsbts(ctx context.Context, + vpkt *tappsbt.VPacket, feeRateSatPerVByte chainfee.SatPerVByte, + changeKeyDesc *keychain.KeyDescriptor, params *chaincfg.Params, + sponsoringInputs []lndclient.LeaseDescriptor, + customLockID *wtxmgr.LockID, lockExpiration time.Duration) ( + *psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket, + *assetwalletrpc.CommitVirtualPsbtsResponse, error) { + + encodedVpkt, err := tappsbt.Encode(vpkt) + if err != nil { + return nil, nil, nil, nil, err + } + + btcPkt, err := tapsend.PrepareAnchoringTemplate( + []*tappsbt.VPacket{vpkt}, + ) + if err != nil { + return nil, nil, nil, nil, err + } + + for _, lease := range sponsoringInputs { + btcPkt.UnsignedTx.TxIn = append( + btcPkt.UnsignedTx.TxIn, &wire.TxIn{ + PreviousOutPoint: lease.Outpoint, + }, + ) + + btcPkt.Inputs = append(btcPkt.Inputs, psbt.PInput{ + WitnessUtxo: wire.NewTxOut( + int64(lease.Value), + lease.PkScript, + ), + }) + } + + commitRequest := &assetwalletrpc.CommitVirtualPsbtsRequest{ + Fees: &assetwalletrpc.CommitVirtualPsbtsRequest_SatPerVbyte{ + SatPerVbyte: uint64(feeRateSatPerVByte), + }, + AnchorChangeOutput: &assetwalletrpc.CommitVirtualPsbtsRequest_Add{ //nolint:lll + Add: true, + }, + VirtualPsbts: [][]byte{ + encodedVpkt, + }, + LockExpirationSeconds: uint64(lockExpiration.Seconds()), + } + + if customLockID != nil { + commitRequest.CustomLockId = (*customLockID)[:] + } + + if feeRateSatPerVByte == 0 { + commitRequest.SkipFunding = true + } + + if changeKeyDesc != nil { + err := addP2WPKHOutputToPsbt( + btcPkt, *changeKeyDesc, btcutil.Amount(1), params, + ) + if err != nil { + return nil, nil, nil, nil, err + } + + commitRequest.AnchorChangeOutput = + &assetwalletrpc.CommitVirtualPsbtsRequest_ExistingOutputIndex{ //nolint:lll + ExistingOutputIndex: 1, + } + } else { + commitRequest.AnchorChangeOutput = + &assetwalletrpc.CommitVirtualPsbtsRequest_Add{ + Add: true, + } + } + var buf bytes.Buffer + err = btcPkt.Serialize(&buf) + if err != nil { + return nil, nil, nil, nil, err + } + + commitRequest.AnchorPsbt = buf.Bytes() + + commitResponse, err := t.AssetWalletClient.CommitVirtualPsbts( + ctx, commitRequest, + ) + if err != nil { + return nil, nil, nil, nil, err + } + + fundedPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(commitResponse.AnchorPsbt), false, + ) + if err != nil { + return nil, nil, nil, nil, err + } + + activePackets := make( + []*tappsbt.VPacket, len(commitResponse.VirtualPsbts), + ) + for idx := range commitResponse.VirtualPsbts { + activePackets[idx], err = tappsbt.Decode( + commitResponse.VirtualPsbts[idx], + ) + if err != nil { + return nil, nil, nil, nil, err + } + } + + passivePackets := make( + []*tappsbt.VPacket, len(commitResponse.PassiveAssetPsbts), + ) + for idx := range commitResponse.PassiveAssetPsbts { + passivePackets[idx], err = tappsbt.Decode( + commitResponse.PassiveAssetPsbts[idx], + ) + if err != nil { + return nil, nil, nil, nil, err + } + } + + return fundedPacket, activePackets, passivePackets, commitResponse, nil +} + +// LogAndPublish logs and publishes a psbt with the given active and passive +// assets. +func (t *TapdClient) LogAndPublish(ctx context.Context, btcPkt *psbt.Packet, + activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket, + commitResp *assetwalletrpc.CommitVirtualPsbtsResponse, + skipBoradcast bool) (*taprpc.SendAssetResponse, error) { + + var buf bytes.Buffer + err := btcPkt.Serialize(&buf) + if err != nil { + return nil, err + } + + request := &assetwalletrpc.PublishAndLogRequest{ + AnchorPsbt: buf.Bytes(), + VirtualPsbts: make([][]byte, len(activeAssets)), + PassiveAssetPsbts: make([][]byte, len(passiveAssets)), + ChangeOutputIndex: commitResp.ChangeOutputIndex, + LndLockedUtxos: commitResp.LndLockedUtxos, + SkipAnchorTxBroadcast: skipBoradcast, + } + + for idx := range activeAssets { + request.VirtualPsbts[idx], err = tappsbt.Encode( + activeAssets[idx], + ) + if err != nil { + return nil, err + } + } + for idx := range passiveAssets { + request.PassiveAssetPsbts[idx], err = tappsbt.Encode( + passiveAssets[idx], + ) + if err != nil { + return nil, err + } + } + + resp, err := t.PublishAndLogTransfer(ctx, request) + if err != nil { + return nil, err + } + + return resp, nil +} + +// GetAssetBalance checks the balance of an asset by its ID. +func (t *TapdClient) GetAssetBalance(ctx context.Context, assetId []byte) ( + uint64, error) { + + // Check if we have enough funds to do the swap. + balanceResp, err := t.ListBalances( + ctx, &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + AssetFilter: assetId, + }, + ) + if err != nil { + return 0, err + } + + // Check if we have enough funds to do the swap. + balance, ok := balanceResp.AssetBalances[hex.EncodeToString( + assetId, + )] + if !ok { + return 0, status.Error(codes.Internal, "internal error") + } + + return balance.Balance, nil +} + +// GetUnEncumberedAssetBalance returns the total balance of the given asset for +// which the given client owns the script keys. +func (t *TapdClient) GetUnEncumberedAssetBalance(ctx context.Context, + assetID []byte) (uint64, error) { + + allAssets, err := t.ListAssets(ctx, &taprpc.ListAssetRequest{}) + if err != nil { + return 0, err + } + + var balance uint64 + for _, a := range allAssets.Assets { + // Only count assets from the given asset ID. + if !bytes.Equal(a.AssetGenesis.AssetId, assetID) { + continue + } + + // Non-local means we don't have the internal key to spend the + // asset. + if !a.ScriptKeyIsLocal { + continue + } + + // If the asset is not declared known or has a script path, we + // can't spend it directly. + if !a.ScriptKeyDeclaredKnown || a.ScriptKeyHasScriptPath { + continue + } + + balance += a.Amount + } + + return balance, nil +} + +// DeriveNewKeys derives a new internal and script key. +func (t *TapdClient) DeriveNewKeys(ctx context.Context) (asset.ScriptKey, + keychain.KeyDescriptor, error) { + + scriptKeyDesc, err := t.NextScriptKey( + ctx, &assetwalletrpc.NextScriptKeyRequest{ + KeyFamily: uint32(asset.TaprootAssetsKeyFamily), + }, + ) + if err != nil { + return asset.ScriptKey{}, keychain.KeyDescriptor{}, err + } + + scriptKey, err := rpcutils.UnmarshalScriptKey(scriptKeyDesc.ScriptKey) + if err != nil { + return asset.ScriptKey{}, keychain.KeyDescriptor{}, err + } + + internalKeyDesc, err := t.NextInternalKey( + ctx, &assetwalletrpc.NextInternalKeyRequest{ + KeyFamily: uint32(asset.TaprootAssetsKeyFamily), + }, + ) + if err != nil { + return asset.ScriptKey{}, keychain.KeyDescriptor{}, err + } + internalKeyLnd, err := rpcutils.UnmarshalKeyDescriptor( + internalKeyDesc.InternalKey, + ) + if err != nil { + return asset.ScriptKey{}, keychain.KeyDescriptor{}, err + } + + return *scriptKey, internalKeyLnd, nil +} + +// ImportProof inserts the given proof to the local tapd instance's database. +func (t *TapdClient) ImportProof(ctx context.Context, p *proof.Proof) error { + var proofBytes bytes.Buffer + err := p.Encode(&proofBytes) + if err != nil { + return err + } + + asset := p.Asset + + proofType := universe.ProofTypeTransfer + if asset.IsGenesisAsset() { + proofType = universe.ProofTypeIssuance + } + + uniID := universe.Identifier{ + AssetID: asset.ID(), + ProofType: proofType, + } + if asset.GroupKey != nil { + uniID.GroupKey = &asset.GroupKey.GroupPubKey + } + + rpcUniID, err := tap.MarshalUniID(uniID) + if err != nil { + return err + } + + outpoint := &universerpc.Outpoint{ + HashStr: p.AnchorTx.TxHash().String(), + Index: int32(p.InclusionProof.OutputIndex), + } + + scriptKey := p.Asset.ScriptKey.PubKey + leafKey := &universerpc.AssetKey{ + Outpoint: &universerpc.AssetKey_Op{ + Op: outpoint, + }, + ScriptKey: &universerpc.AssetKey_ScriptKeyBytes{ + ScriptKeyBytes: scriptKey.SerializeCompressed(), + }, + } + + _, err = t.InsertProof(ctx, &universerpc.AssetProof{ + Key: &universerpc.UniverseKey{ + Id: rpcUniID, + LeafKey: leafKey, + }, + AssetLeaf: &universerpc.AssetLeaf{ + Proof: proofBytes.Bytes(), + }, + }) + + return err +} + +// ImportProofFile imports the proof file and returns the last proof. +func (t *TapdClient) ImportProofFile(ctx context.Context, rawProofFile []byte) ( + *proof.Proof, error) { + + proofFile, err := proof.DecodeFile(rawProofFile) + if err != nil { + return nil, err + } + + var lastProof *proof.Proof + + for i := 0; i < proofFile.NumProofs(); i++ { + lastProof, err = proofFile.ProofAt(uint32(i)) + if err != nil { + return nil, err + } + + err = t.ImportProof(ctx, lastProof) + if err != nil { + return nil, err + } + } + + return lastProof, nil +} + // getPaymentMaxAmount returns the milisat amount we are willing to pay for the // payment. func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( @@ -277,39 +767,147 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( ) } -func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { - // Load the specified TLS certificate and build transport credentials. - creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") - if err != nil { - return nil, err - } +// TapReceiveEvent is a struct that holds the information about a receive event. +type TapReceiveEvent struct { + // Outpoint is the anchor outpoint containing the confirmed asset. + Outpoint wire.OutPoint - // Load the specified macaroon file. - macBytes, err := os.ReadFile(config.MacaroonPath) + // ConfirmationHeight is the height at which the asset transfer was + // confirmed. + ConfirmationHeight uint32 +} + +// WaitForReceiveComplete waits for a receive to complete returning a channel +// that will notify the caller when the receive is complete. The addr is +// the address to filter for, and startTs is the timestamp from which to +// start receiving events. +func (t *TapdClient) WaitForReceiveComplete(ctx context.Context, addr string, + startTs time.Time) (<-chan TapReceiveEvent, <-chan error, error) { + + receiveEventsClient, err := t.SubscribeReceiveEvents( + ctx, &taprpc.SubscribeReceiveEventsRequest{ + FilterAddr: addr, + StartTimestamp: startTs.UnixMicro(), + }, + ) if err != nil { - return nil, err + return nil, nil, err } - mac := &macaroon.Macaroon{} - if err := mac.UnmarshalBinary(macBytes); err != nil { - return nil, err + + resChan := make(chan TapReceiveEvent) + errChan := make(chan error, 1) + + go func() { + for { + select { + case <-receiveEventsClient.Context().Done(): + panic(receiveEventsClient.Context().Err()) + default: + } + event, err := receiveEventsClient.Recv() + if err != nil { + errChan <- err + + return + } + + done, err := handleReceiveEvent(event, resChan) + if err != nil { + errChan <- err + + return + } + + if done { + return + } + } + }() + + return resChan, errChan, err +} + +func handleReceiveEvent(event *taprpc.ReceiveEvent, + resChan chan<- TapReceiveEvent) (bool, error) { + + switch event.Status { + case taprpc.AddrEventStatus_ADDR_EVENT_STATUS_TRANSACTION_DETECTED: + + case taprpc.AddrEventStatus_ADDR_EVENT_STATUS_TRANSACTION_CONFIRMED: + + case taprpc.AddrEventStatus_ADDR_EVENT_STATUS_COMPLETED: + outpoint, err := wire.NewOutPointFromString(event.Outpoint) + if err != nil { + return false, err + } + + resChan <- TapReceiveEvent{ + Outpoint: *outpoint, + ConfirmationHeight: event.ConfirmationHeight, + } + + return true, nil + + default: } - macaroon, err := macaroons.NewMacaroonCredential(mac) + return false, nil +} + +// TapSendEvent is a struct that holds the information about a send event. +type TapSendEvent struct { + Transfer *taprpc.AssetTransfer +} + +// WaitForSendComplete waits for a send to complete returning a channel that +// will notify the caller when the send is complete. The filterScriptKey is +// the script key of the asset to filter for, and the filterLabel is an +// optional label to filter the send events by. +func (t *TapdClient) WaitForSendComplete(ctx context.Context, + filterScriptKey []byte, filterLabel string) (<-chan TapSendEvent, + <-chan error, error) { + + sendEventsClient, err := t.SubscribeSendEvents( + ctx, &taprpc.SubscribeSendEventsRequest{ + FilterScriptKey: filterScriptKey, + FilterLabel: filterLabel, + }, + ) if err != nil { - return nil, err - } - // Create the DialOptions with the macaroon credentials. - opts := []grpc.DialOption{ - grpc.WithTransportCredentials(creds), - grpc.WithPerRPCCredentials(macaroon), - grpc.WithDefaultCallOptions(maxMsgRecvSize), + return nil, nil, err } - // Dial the gRPC server. - conn, err := grpc.Dial(config.Host, opts...) - if err != nil { - return nil, err + resChan := make(chan TapSendEvent) + errChan := make(chan error, 1) + + go func() { + for { + event, err := sendEventsClient.Recv() + if err != nil { + errChan <- err + + return + } + + if handleSendEvent(event, resChan) { + return + } + } + }() + + return resChan, errChan, nil +} + +func handleSendEvent(event *taprpc.SendEvent, + resChan chan<- TapSendEvent) bool { + + if event.SendState == tapfreighter.SendStateComplete.String() { + resChan <- TapSendEvent{ + Transfer: event.Transfer, + } + + return true } - return conn, nil + return false } diff --git a/assets/tapkit.go b/assets/tapkit.go new file mode 100644 index 000000000..c5498b47f --- /dev/null +++ b/assets/tapkit.go @@ -0,0 +1,158 @@ +package assets + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/assets/htlc" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tappsbt" + "github.com/lightninglabs/taproot-assets/tapsend" +) + +// GenTaprootAssetRootFromProof generates the taproot asset root from the proof +// of the swap. +func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) { + assetCopy := proof.Asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets(&version, assetCopy) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot := assetCommitment.TapscriptRoot(nil) + + return taprootAssetRoot[:], nil +} + +// CreateOpTrueSweepVpkt creates a VPacket that sweeps the outputs associated +// with the passed in proofs, given that their TAP script is a simple OP_TRUE. +func CreateOpTrueSweepVpkt(ctx context.Context, proofs []*proof.Proof, + addr *address.Tap, chainParams *address.ChainParams) ( + *tappsbt.VPacket, error) { + + sweepVpkt, err := tappsbt.FromProofs(proofs, chainParams, tappsbt.V1) + if err != nil { + return nil, err + } + + total := uint64(0) + for i, proof := range proofs { + inputKey := proof.InclusionProof.InternalKey + + sweepVpkt.Inputs[i].Anchor.Bip32Derivation = + []*psbt.Bip32Derivation{ + { + PubKey: inputKey.SerializeCompressed(), + }, + } + sweepVpkt.Inputs[i].Anchor.TrBip32Derivation = + []*psbt.TaprootBip32Derivation{ + { + XOnlyPubKey: schnorr.SerializePubKey( + inputKey, + ), + }, + } + + total += proof.Asset.Amount + } + + // Sanity check that the amount that we're attempting to sweep matches + // the address amount. + if total != addr.Amount { + return nil, fmt.Errorf("total amount of proofs does not " + + "match the amount of the address") + } + + /* + addressRecvVpkt, err := tappsbt.FromAddresses([]*address.Tap{addr}, 0) + if err != nil { + return nil, err + } + + sweepVpkt.Outputs = addressRecvVpkt.Outputs + */ + + // If we are sending the full value of the input asset, or sending a + // collectible, we will need to create a split with un-spendable change. + // Since we don't have any inputs selected yet, we'll use the NUMS + // script key to avoid deriving a new key for each funding attempt. If + // we need a change output, this un-spendable script key will be + // identified as such and replaced with a real one during the funding + // process. + sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{ + Amount: 0, + Interactive: false, + Type: tappsbt.TypeSplitRoot, + AnchorOutputIndex: 0, + ScriptKey: asset.NUMSScriptKey, + // TODO(bhandras): set this to the actual internal key derived + // from the sender node, otherwise they'll lose the 1000 sats + // of the tombstone output. + AnchorOutputInternalKey: asset.NUMSPubKey, + }) + + sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{ + AssetVersion: addr.AssetVersion, + Amount: addr.Amount, + Interactive: false, + AnchorOutputIndex: 1, + ScriptKey: asset.NewScriptKey( + &addr.ScriptKey, + ), + AnchorOutputInternalKey: &addr.InternalKey, + AnchorOutputTapscriptSibling: addr.TapscriptSibling, + ProofDeliveryAddress: &addr.ProofCourierAddr, + }) + + err = tapsend.PrepareOutputAssets(ctx, sweepVpkt) + if err != nil { + return nil, err + } + + _, _, _, controlBlock, err := htlc.CreateOpTrueLeaf() + if err != nil { + return nil, err + } + + controlBlockBytes, err := controlBlock.ToBytes() + if err != nil { + return nil, err + } + + opTrueScript, err := htlc.GetOpTrueScript() + if err != nil { + return nil, err + } + + witness := wire.TxWitness{ + opTrueScript, + controlBlockBytes, + } + + err = sweepVpkt.Outputs[0].Asset.UpdateTxWitness(0, witness) + if err != nil { + return nil, fmt.Errorf("unable to update witness: %w", err) + } + + err = sweepVpkt.Outputs[1].Asset.UpdateTxWitness(0, witness) + if err != nil { + return nil, fmt.Errorf("unable to update witness: %w", err) + } + return sweepVpkt, nil +}