From 53aa089b2172240f56774de09613e71c0f2ec896 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:09:46 +0200 Subject: [PATCH 01/19] 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 83c66fd0bf2bb1b3de7681bb89d7a65860107628 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:18:46 +0200 Subject: [PATCH 02/19] 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 0e7e413ce9e22a06d91f8594a52dc54e9891095c Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:32:57 +0200 Subject: [PATCH 03/19] 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 +} From 070b2a9ad08d02ee22a732a4ce3fb61e9060e592 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:41:11 +0200 Subject: [PATCH 04/19] swapserverrpc: add proto definitions for server asset deposit RPCs --- swapserverrpc/asset_deposit.pb.go | 881 +++++++++++++++++++++++++ swapserverrpc/asset_deposit.proto | 121 ++++ swapserverrpc/asset_deposit_grpc.pb.go | 221 +++++++ 3 files changed, 1223 insertions(+) create mode 100644 swapserverrpc/asset_deposit.pb.go create mode 100644 swapserverrpc/asset_deposit.proto create mode 100644 swapserverrpc/asset_deposit_grpc.pb.go diff --git a/swapserverrpc/asset_deposit.pb.go b/swapserverrpc/asset_deposit.pb.go new file mode 100644 index 000000000..7ff7886cd --- /dev/null +++ b/swapserverrpc/asset_deposit.pb.go @@ -0,0 +1,881 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.21.12 +// source: asset_deposit.proto + +// We can't change this to swapserverrpc, it would be a breaking change because +// the package name is also contained in the HTTP URIs and old clients would +// call the wrong endpoints. Luckily with the go_package option we can have +// different golang and RPC package names to fix protobuf namespace conflicts. + +package swapserverrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AssetDepositProtocolVersion is the version of the asset deposit protocol. +type AssetDepositProtocolVersion int32 + +const ( + // V0 is the first version of the asset deposit protocol. + AssetDepositProtocolVersion_ASSET_DEPOSIT_V0 AssetDepositProtocolVersion = 0 +) + +// Enum value maps for AssetDepositProtocolVersion. +var ( + AssetDepositProtocolVersion_name = map[int32]string{ + 0: "ASSET_DEPOSIT_V0", + } + AssetDepositProtocolVersion_value = map[string]int32{ + "ASSET_DEPOSIT_V0": 0, + } +) + +func (x AssetDepositProtocolVersion) Enum() *AssetDepositProtocolVersion { + p := new(AssetDepositProtocolVersion) + *p = x + return p +} + +func (x AssetDepositProtocolVersion) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AssetDepositProtocolVersion) Descriptor() protoreflect.EnumDescriptor { + return file_asset_deposit_proto_enumTypes[0].Descriptor() +} + +func (AssetDepositProtocolVersion) Type() protoreflect.EnumType { + return &file_asset_deposit_proto_enumTypes[0] +} + +func (x AssetDepositProtocolVersion) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AssetDepositProtocolVersion.Descriptor instead. +func (AssetDepositProtocolVersion) EnumDescriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{0} +} + +// NewAssetDepositServerReq is the request to the Server to create a new asset +// deposit. +type NewAssetDepositServerReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // asset_id is the id of the asset to deposit. + AssetId []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + // amount is the amount of the asset to deposit. + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` + // client_internal_key is the client's internal pubkey used for the asset + // deposit deposit MuSig2 key. + ClientInternalPubkey []byte `protobuf:"bytes,3,opt,name=client_internal_pubkey,json=clientInternalPubkey,proto3" json:"client_internal_pubkey,omitempty"` + // client_script_key is the client's script pubkey used for the asset + // deposit timeout script. + ClientScriptPubkey []byte `protobuf:"bytes,4,opt,name=client_script_pubkey,json=clientScriptPubkey,proto3" json:"client_script_pubkey,omitempty"` + // csv_expiry is the CSV expiry for the deposit transaction. + CsvExpiry int32 `protobuf:"varint,5,opt,name=csv_expiry,json=csvExpiry,proto3" json:"csv_expiry,omitempty"` +} + +func (x *NewAssetDepositServerReq) Reset() { + *x = NewAssetDepositServerReq{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewAssetDepositServerReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewAssetDepositServerReq) ProtoMessage() {} + +func (x *NewAssetDepositServerReq) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewAssetDepositServerReq.ProtoReflect.Descriptor instead. +func (*NewAssetDepositServerReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{0} +} + +func (x *NewAssetDepositServerReq) GetAssetId() []byte { + if x != nil { + return x.AssetId + } + return nil +} + +func (x *NewAssetDepositServerReq) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *NewAssetDepositServerReq) GetClientInternalPubkey() []byte { + if x != nil { + return x.ClientInternalPubkey + } + return nil +} + +func (x *NewAssetDepositServerReq) GetClientScriptPubkey() []byte { + if x != nil { + return x.ClientScriptPubkey + } + return nil +} + +func (x *NewAssetDepositServerReq) GetCsvExpiry() int32 { + if x != nil { + return x.CsvExpiry + } + return 0 +} + +// NewAssetDepositServerRes is the Server's response to a NewAssetDeposit +// request. +type NewAssetDepositServerRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // deposit_id is the unique id of the deposit. + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` + // server_script_pubkey is the script pubkey of the server used for the + // asset deposit spending HTLC script. + ServerScriptPubkey []byte `protobuf:"bytes,2,opt,name=server_script_pubkey,json=serverScriptPubkey,proto3" json:"server_script_pubkey,omitempty"` + // server_internal_pubkey is the public key of the server used for the asset + // deposit MuSig2 key. + ServerInternalPubkey []byte `protobuf:"bytes,3,opt,name=server_internal_pubkey,json=serverInternalPubkey,proto3" json:"server_internal_pubkey,omitempty"` + // deposit_addr is the TAP address to deposit the asset to. + DepositAddr string `protobuf:"bytes,4,opt,name=deposit_addr,json=depositAddr,proto3" json:"deposit_addr,omitempty"` +} + +func (x *NewAssetDepositServerRes) Reset() { + *x = NewAssetDepositServerRes{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewAssetDepositServerRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewAssetDepositServerRes) ProtoMessage() {} + +func (x *NewAssetDepositServerRes) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewAssetDepositServerRes.ProtoReflect.Descriptor instead. +func (*NewAssetDepositServerRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{1} +} + +func (x *NewAssetDepositServerRes) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +func (x *NewAssetDepositServerRes) GetServerScriptPubkey() []byte { + if x != nil { + return x.ServerScriptPubkey + } + return nil +} + +func (x *NewAssetDepositServerRes) GetServerInternalPubkey() []byte { + if x != nil { + return x.ServerInternalPubkey + } + return nil +} + +func (x *NewAssetDepositServerRes) GetDepositAddr() string { + if x != nil { + return x.DepositAddr + } + return "" +} + +type WithdrawAssetDepositsServerReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositIds []string `protobuf:"bytes,1,rep,name=deposit_ids,json=depositIds,proto3" json:"deposit_ids,omitempty"` +} + +func (x *WithdrawAssetDepositsServerReq) Reset() { + *x = WithdrawAssetDepositsServerReq{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WithdrawAssetDepositsServerReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WithdrawAssetDepositsServerReq) ProtoMessage() {} + +func (x *WithdrawAssetDepositsServerReq) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WithdrawAssetDepositsServerReq.ProtoReflect.Descriptor instead. +func (*WithdrawAssetDepositsServerReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{2} +} + +func (x *WithdrawAssetDepositsServerReq) GetDepositIds() []string { + if x != nil { + return x.DepositIds + } + return nil +} + +type WithdrawAssetDepositsServerRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositKeys map[string][]byte `protobuf:"bytes,1,rep,name=deposit_keys,json=depositKeys,proto3" json:"deposit_keys,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *WithdrawAssetDepositsServerRes) Reset() { + *x = WithdrawAssetDepositsServerRes{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WithdrawAssetDepositsServerRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WithdrawAssetDepositsServerRes) ProtoMessage() {} + +func (x *WithdrawAssetDepositsServerRes) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WithdrawAssetDepositsServerRes.ProtoReflect.Descriptor instead. +func (*WithdrawAssetDepositsServerRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{3} +} + +func (x *WithdrawAssetDepositsServerRes) GetDepositKeys() map[string][]byte { + if x != nil { + return x.DepositKeys + } + return nil +} + +// AssetDepositPartialSig holds a nonce and partial signature spending a +// deposit. +type AssetDepositPartialSig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // deposit_id is the deposit ID corresponding to this partial signature. + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` + // nonce is the nonce used for generating this signature. + Nonce []byte `protobuf:"bytes,2,opt,name=nonce,proto3" json:"nonce,omitempty"` + // partial_sig is the partial signature for spending the deposit. + PartialSig []byte `protobuf:"bytes,3,opt,name=partial_sig,json=partialSig,proto3" json:"partial_sig,omitempty"` +} + +func (x *AssetDepositPartialSig) Reset() { + *x = AssetDepositPartialSig{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AssetDepositPartialSig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetDepositPartialSig) ProtoMessage() {} + +func (x *AssetDepositPartialSig) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetDepositPartialSig.ProtoReflect.Descriptor instead. +func (*AssetDepositPartialSig) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{4} +} + +func (x *AssetDepositPartialSig) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +func (x *AssetDepositPartialSig) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +func (x *AssetDepositPartialSig) GetPartialSig() []byte { + if x != nil { + return x.PartialSig + } + return nil +} + +// PushAssetDepositHtlcSigsReq holds partial signatures spending one or more +// deposits and the zero fee HTLC spending them. +type PushAssetDepositHtlcSigsReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // partial_sigs holds the partial signatures for the deposits spent by the + // HTLC. The inputs of the HTLC will be in the same order defined here. + PartialSigs []*AssetDepositPartialSig `protobuf:"bytes,1,rep,name=partial_sigs,json=partialSigs,proto3" json:"partial_sigs,omitempty"` + // htlc_psbt is the HTLC psbt. + HtlcPsbt []byte `protobuf:"bytes,2,opt,name=htlc_psbt,json=htlcPsbt,proto3" json:"htlc_psbt,omitempty"` +} + +func (x *PushAssetDepositHtlcSigsReq) Reset() { + *x = PushAssetDepositHtlcSigsReq{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositHtlcSigsReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositHtlcSigsReq) ProtoMessage() {} + +func (x *PushAssetDepositHtlcSigsReq) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushAssetDepositHtlcSigsReq.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigsReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{5} +} + +func (x *PushAssetDepositHtlcSigsReq) GetPartialSigs() []*AssetDepositPartialSig { + if x != nil { + return x.PartialSigs + } + return nil +} + +func (x *PushAssetDepositHtlcSigsReq) GetHtlcPsbt() []byte { + if x != nil { + return x.HtlcPsbt + } + return nil +} + +type PushAssetDepositHtlcSigsRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *PushAssetDepositHtlcSigsRes) Reset() { + *x = PushAssetDepositHtlcSigsRes{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositHtlcSigsRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositHtlcSigsRes) ProtoMessage() {} + +func (x *PushAssetDepositHtlcSigsRes) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushAssetDepositHtlcSigsRes.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigsRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{6} +} + +// PushAssetDepositKeysReq holds private keys of one or more deposits. +type PushAssetDepositKeysReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // deposit_keys is a map wich maps deposit_id to deposit internal private + // key. + DepositKeys map[string][]byte `protobuf:"bytes,1,rep,name=deposit_keys,json=depositKeys,proto3" json:"deposit_keys,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *PushAssetDepositKeysReq) Reset() { + *x = PushAssetDepositKeysReq{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositKeysReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositKeysReq) ProtoMessage() {} + +func (x *PushAssetDepositKeysReq) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushAssetDepositKeysReq.ProtoReflect.Descriptor instead. +func (*PushAssetDepositKeysReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{7} +} + +func (x *PushAssetDepositKeysReq) GetDepositKeys() map[string][]byte { + if x != nil { + return x.DepositKeys + } + return nil +} + +type PushAssetDepositKeysRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *PushAssetDepositKeysRes) Reset() { + *x = PushAssetDepositKeysRes{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositKeysRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositKeysRes) ProtoMessage() {} + +func (x *PushAssetDepositKeysRes) ProtoReflect() protoreflect.Message { + mi := &file_asset_deposit_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PushAssetDepositKeysRes.ProtoReflect.Descriptor instead. +func (*PushAssetDepositKeysRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{8} +} + +var File_asset_deposit_proto protoreflect.FileDescriptor + +var file_asset_deposit_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0xd4, + 0x01, 0x0a, 0x18, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x19, 0x0a, 0x08, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x34, + 0x0a, 0x16, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x50, 0x75, + 0x62, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, 0x76, 0x5f, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x73, 0x76, 0x45, + 0x78, 0x70, 0x69, 0x72, 0x79, 0x22, 0xc4, 0x01, 0x0a, 0x18, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, + 0x64, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x12, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, + 0x6b, 0x65, 0x79, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x14, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x41, 0x64, 0x64, 0x72, 0x22, 0x41, 0x0a, 0x1e, + 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1f, + 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, 0x64, 0x73, 0x22, + 0xbd, 0x01, 0x0a, 0x1e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x12, 0x5b, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x6b, 0x65, + 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x2e, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x1a, + 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x6e, 0x0a, 0x16, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x50, + 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x22, + 0x7e, 0x0a, 0x1b, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x12, 0x42, + 0x0a, 0x0c, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x50, 0x61, 0x72, 0x74, 0x69, + 0x61, 0x6c, 0x53, 0x69, 0x67, 0x52, 0x0b, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x69, + 0x67, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x68, 0x74, 0x6c, 0x63, 0x50, 0x73, 0x62, 0x74, 0x22, + 0x1d, 0x0a, 0x1b, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x22, 0xaf, + 0x01, 0x0a, 0x17, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x12, 0x54, 0x0a, 0x0c, 0x64, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, + 0x65, 0x71, 0x2e, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, + 0x1a, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0x19, 0x0a, 0x17, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x2a, 0x33, 0x0a, 0x1b, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x53, + 0x53, 0x45, 0x54, 0x5f, 0x44, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x5f, 0x56, 0x30, 0x10, 0x00, + 0x32, 0x9d, 0x03, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x57, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x12, 0x21, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x21, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x12, 0x69, 0x0a, 0x15, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x6c, 0x6f, 0x6f, + 0x70, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x1a, 0x27, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x69, + 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x12, 0x66, 0x0a, 0x18, + 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x12, 0x24, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, + 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x24, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, + 0x73, 0x52, 0x65, 0x73, 0x12, 0x5a, 0x0a, 0x14, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x20, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x20, + 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, + 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, + 0x70, 0x2f, 0x73, 0x77, 0x61, 0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_asset_deposit_proto_rawDescOnce sync.Once + file_asset_deposit_proto_rawDescData = file_asset_deposit_proto_rawDesc +) + +func file_asset_deposit_proto_rawDescGZIP() []byte { + file_asset_deposit_proto_rawDescOnce.Do(func() { + file_asset_deposit_proto_rawDescData = protoimpl.X.CompressGZIP(file_asset_deposit_proto_rawDescData) + }) + return file_asset_deposit_proto_rawDescData +} + +var file_asset_deposit_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_asset_deposit_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_asset_deposit_proto_goTypes = []any{ + (AssetDepositProtocolVersion)(0), // 0: looprpc.AssetDepositProtocolVersion + (*NewAssetDepositServerReq)(nil), // 1: looprpc.NewAssetDepositServerReq + (*NewAssetDepositServerRes)(nil), // 2: looprpc.NewAssetDepositServerRes + (*WithdrawAssetDepositsServerReq)(nil), // 3: looprpc.WithdrawAssetDepositsServerReq + (*WithdrawAssetDepositsServerRes)(nil), // 4: looprpc.WithdrawAssetDepositsServerRes + (*AssetDepositPartialSig)(nil), // 5: looprpc.AssetDepositPartialSig + (*PushAssetDepositHtlcSigsReq)(nil), // 6: looprpc.PushAssetDepositHtlcSigsReq + (*PushAssetDepositHtlcSigsRes)(nil), // 7: looprpc.PushAssetDepositHtlcSigsRes + (*PushAssetDepositKeysReq)(nil), // 8: looprpc.PushAssetDepositKeysReq + (*PushAssetDepositKeysRes)(nil), // 9: looprpc.PushAssetDepositKeysRes + nil, // 10: looprpc.WithdrawAssetDepositsServerRes.DepositKeysEntry + nil, // 11: looprpc.PushAssetDepositKeysReq.DepositKeysEntry +} +var file_asset_deposit_proto_depIdxs = []int32{ + 10, // 0: looprpc.WithdrawAssetDepositsServerRes.deposit_keys:type_name -> looprpc.WithdrawAssetDepositsServerRes.DepositKeysEntry + 5, // 1: looprpc.PushAssetDepositHtlcSigsReq.partial_sigs:type_name -> looprpc.AssetDepositPartialSig + 11, // 2: looprpc.PushAssetDepositKeysReq.deposit_keys:type_name -> looprpc.PushAssetDepositKeysReq.DepositKeysEntry + 1, // 3: looprpc.AssetDepositService.NewAssetDeposit:input_type -> looprpc.NewAssetDepositServerReq + 3, // 4: looprpc.AssetDepositService.WithdrawAssetDeposits:input_type -> looprpc.WithdrawAssetDepositsServerReq + 6, // 5: looprpc.AssetDepositService.PushAssetDepositHtlcSigs:input_type -> looprpc.PushAssetDepositHtlcSigsReq + 8, // 6: looprpc.AssetDepositService.PushAssetDepositKeys:input_type -> looprpc.PushAssetDepositKeysReq + 2, // 7: looprpc.AssetDepositService.NewAssetDeposit:output_type -> looprpc.NewAssetDepositServerRes + 4, // 8: looprpc.AssetDepositService.WithdrawAssetDeposits:output_type -> looprpc.WithdrawAssetDepositsServerRes + 7, // 9: looprpc.AssetDepositService.PushAssetDepositHtlcSigs:output_type -> looprpc.PushAssetDepositHtlcSigsRes + 9, // 10: looprpc.AssetDepositService.PushAssetDepositKeys:output_type -> looprpc.PushAssetDepositKeysRes + 7, // [7:11] is the sub-list for method output_type + 3, // [3:7] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_asset_deposit_proto_init() } +func file_asset_deposit_proto_init() { + if File_asset_deposit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_asset_deposit_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*NewAssetDepositServerReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*NewAssetDepositServerRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*WithdrawAssetDepositsServerReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*WithdrawAssetDepositsServerRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*AssetDepositPartialSig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*PushAssetDepositHtlcSigsReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*PushAssetDepositHtlcSigsRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*PushAssetDepositKeysReq); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_asset_deposit_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*PushAssetDepositKeysRes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_asset_deposit_proto_rawDesc, + NumEnums: 1, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_asset_deposit_proto_goTypes, + DependencyIndexes: file_asset_deposit_proto_depIdxs, + EnumInfos: file_asset_deposit_proto_enumTypes, + MessageInfos: file_asset_deposit_proto_msgTypes, + }.Build() + File_asset_deposit_proto = out.File + file_asset_deposit_proto_rawDesc = nil + file_asset_deposit_proto_goTypes = nil + file_asset_deposit_proto_depIdxs = nil +} diff --git a/swapserverrpc/asset_deposit.proto b/swapserverrpc/asset_deposit.proto new file mode 100644 index 000000000..20c87fa4f --- /dev/null +++ b/swapserverrpc/asset_deposit.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +// We can't change this to swapserverrpc, it would be a breaking change because +// the package name is also contained in the HTTP URIs and old clients would +// call the wrong endpoints. Luckily with the go_package option we can have +// different golang and RPC package names to fix protobuf namespace conflicts. +package looprpc; + +option go_package = "github.com/lightninglabs/loop/swapserverrpc"; + +// AssetDepositService is the service handling asset deposit creation and +// spending. Asset deposits are used in asset loop-in swaps. +service AssetDepositService { + // NewAssetDeposit creates a new asset deposit address. + rpc NewAssetDeposit (NewAssetDepositServerReq) + returns (NewAssetDepositServerRes); + + // WithdrawAssetDeposit withdraws asset deposits to the user's wallet. + rpc WithdrawAssetDeposits (WithdrawAssetDepositsServerReq) + returns (WithdrawAssetDepositsServerRes); + + // PushHtlcSigs pushes a MuSig2 partial signatures to the server spending + // one ore more deposits to a zero fee HTLC. + rpc PushAssetDepositHtlcSigs (PushAssetDepositHtlcSigsReq) + returns (PushAssetDepositHtlcSigsRes); + + // PushKeys pushes (ie reveals) the private keys of one ore more deposits to + // the server. + rpc PushAssetDepositKeys (PushAssetDepositKeysReq) + returns (PushAssetDepositKeysRes); +} + +// NewAssetDepositServerReq is the request to the Server to create a new asset +// deposit. +message NewAssetDepositServerReq { + // asset_id is the id of the asset to deposit. + bytes asset_id = 1; + + // amount is the amount of the asset to deposit. + uint64 amount = 2; + + // client_internal_key is the client's internal pubkey used for the asset + // deposit deposit MuSig2 key. + bytes client_internal_pubkey = 3; + + // client_script_key is the client's script pubkey used for the asset + // deposit timeout script. + bytes client_script_pubkey = 4; + + // csv_expiry is the CSV expiry for the deposit transaction. + int32 csv_expiry = 5; +} + +// NewAssetDepositServerRes is the Server's response to a NewAssetDeposit +// request. +message NewAssetDepositServerRes { + // deposit_id is the unique id of the deposit. + string deposit_id = 1; + + // server_script_pubkey is the script pubkey of the server used for the + // asset deposit spending HTLC script. + bytes server_script_pubkey = 2; + + // server_internal_pubkey is the public key of the server used for the asset + // deposit MuSig2 key. + bytes server_internal_pubkey = 3; + + // deposit_addr is the TAP address to deposit the asset to. + string deposit_addr = 4; +} + +message WithdrawAssetDepositsServerReq { + repeated string deposit_ids = 1; +} + +message WithdrawAssetDepositsServerRes { + map deposit_keys = 1; +} + +// AssetDepositPartialSig holds a nonce and partial signature spending a +// deposit. +message AssetDepositPartialSig { + // deposit_id is the deposit ID corresponding to this partial signature. + string deposit_id = 1; + + // nonce is the nonce used for generating this signature. + bytes nonce = 2; + + // partial_sig is the partial signature for spending the deposit. + bytes partial_sig = 3; +} + +// PushAssetDepositHtlcSigsReq holds partial signatures spending one or more +// deposits and the zero fee HTLC spending them. +message PushAssetDepositHtlcSigsReq { + // partial_sigs holds the partial signatures for the deposits spent by the + // HTLC. The inputs of the HTLC will be in the same order defined here. + repeated AssetDepositPartialSig partial_sigs = 1; + + // htlc_psbt is the HTLC psbt. + bytes htlc_psbt = 2; +} + +message PushAssetDepositHtlcSigsRes { +} + +// PushAssetDepositKeysReq holds private keys of one or more deposits. +message PushAssetDepositKeysReq { + // deposit_keys is a map wich maps deposit_id to deposit internal private + // key. + map deposit_keys = 1; +} + +message PushAssetDepositKeysRes { +} + +// AssetDepositProtocolVersion is the version of the asset deposit protocol. +enum AssetDepositProtocolVersion { + // V0 is the first version of the asset deposit protocol. + ASSET_DEPOSIT_V0 = 0; +}; diff --git a/swapserverrpc/asset_deposit_grpc.pb.go b/swapserverrpc/asset_deposit_grpc.pb.go new file mode 100644 index 000000000..03dcd3e57 --- /dev/null +++ b/swapserverrpc/asset_deposit_grpc.pb.go @@ -0,0 +1,221 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package swapserverrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// AssetDepositServiceClient is the client API for AssetDepositService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AssetDepositServiceClient interface { + // NewAssetDeposit creates a new asset deposit address. + NewAssetDeposit(ctx context.Context, in *NewAssetDepositServerReq, opts ...grpc.CallOption) (*NewAssetDepositServerRes, error) + // WithdrawAssetDeposit withdraws asset deposits to the user's wallet. + WithdrawAssetDeposits(ctx context.Context, in *WithdrawAssetDepositsServerReq, opts ...grpc.CallOption) (*WithdrawAssetDepositsServerRes, error) + // PushHtlcSigs pushes a MuSig2 partial signatures to the server spending + // one ore more deposits to a zero fee HTLC. + PushAssetDepositHtlcSigs(ctx context.Context, in *PushAssetDepositHtlcSigsReq, opts ...grpc.CallOption) (*PushAssetDepositHtlcSigsRes, error) + // PushKeys pushes (ie reveals) the private keys of one ore more deposits to + // the server. + PushAssetDepositKeys(ctx context.Context, in *PushAssetDepositKeysReq, opts ...grpc.CallOption) (*PushAssetDepositKeysRes, error) +} + +type assetDepositServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAssetDepositServiceClient(cc grpc.ClientConnInterface) AssetDepositServiceClient { + return &assetDepositServiceClient{cc} +} + +func (c *assetDepositServiceClient) NewAssetDeposit(ctx context.Context, in *NewAssetDepositServerReq, opts ...grpc.CallOption) (*NewAssetDepositServerRes, error) { + out := new(NewAssetDepositServerRes) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositService/NewAssetDeposit", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositServiceClient) WithdrawAssetDeposits(ctx context.Context, in *WithdrawAssetDepositsServerReq, opts ...grpc.CallOption) (*WithdrawAssetDepositsServerRes, error) { + out := new(WithdrawAssetDepositsServerRes) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositService/WithdrawAssetDeposits", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositServiceClient) PushAssetDepositHtlcSigs(ctx context.Context, in *PushAssetDepositHtlcSigsReq, opts ...grpc.CallOption) (*PushAssetDepositHtlcSigsRes, error) { + out := new(PushAssetDepositHtlcSigsRes) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositService/PushAssetDepositHtlcSigs", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositServiceClient) PushAssetDepositKeys(ctx context.Context, in *PushAssetDepositKeysReq, opts ...grpc.CallOption) (*PushAssetDepositKeysRes, error) { + out := new(PushAssetDepositKeysRes) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositService/PushAssetDepositKeys", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AssetDepositServiceServer is the server API for AssetDepositService service. +// All implementations must embed UnimplementedAssetDepositServiceServer +// for forward compatibility +type AssetDepositServiceServer interface { + // NewAssetDeposit creates a new asset deposit address. + NewAssetDeposit(context.Context, *NewAssetDepositServerReq) (*NewAssetDepositServerRes, error) + // WithdrawAssetDeposit withdraws asset deposits to the user's wallet. + WithdrawAssetDeposits(context.Context, *WithdrawAssetDepositsServerReq) (*WithdrawAssetDepositsServerRes, error) + // PushHtlcSigs pushes a MuSig2 partial signatures to the server spending + // one ore more deposits to a zero fee HTLC. + PushAssetDepositHtlcSigs(context.Context, *PushAssetDepositHtlcSigsReq) (*PushAssetDepositHtlcSigsRes, error) + // PushKeys pushes (ie reveals) the private keys of one ore more deposits to + // the server. + PushAssetDepositKeys(context.Context, *PushAssetDepositKeysReq) (*PushAssetDepositKeysRes, error) + mustEmbedUnimplementedAssetDepositServiceServer() +} + +// UnimplementedAssetDepositServiceServer must be embedded to have forward compatible implementations. +type UnimplementedAssetDepositServiceServer struct { +} + +func (UnimplementedAssetDepositServiceServer) NewAssetDeposit(context.Context, *NewAssetDepositServerReq) (*NewAssetDepositServerRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method NewAssetDeposit not implemented") +} +func (UnimplementedAssetDepositServiceServer) WithdrawAssetDeposits(context.Context, *WithdrawAssetDepositsServerReq) (*WithdrawAssetDepositsServerRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method WithdrawAssetDeposits not implemented") +} +func (UnimplementedAssetDepositServiceServer) PushAssetDepositHtlcSigs(context.Context, *PushAssetDepositHtlcSigsReq) (*PushAssetDepositHtlcSigsRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method PushAssetDepositHtlcSigs not implemented") +} +func (UnimplementedAssetDepositServiceServer) PushAssetDepositKeys(context.Context, *PushAssetDepositKeysReq) (*PushAssetDepositKeysRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method PushAssetDepositKeys not implemented") +} +func (UnimplementedAssetDepositServiceServer) mustEmbedUnimplementedAssetDepositServiceServer() {} + +// UnsafeAssetDepositServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AssetDepositServiceServer will +// result in compilation errors. +type UnsafeAssetDepositServiceServer interface { + mustEmbedUnimplementedAssetDepositServiceServer() +} + +func RegisterAssetDepositServiceServer(s grpc.ServiceRegistrar, srv AssetDepositServiceServer) { + s.RegisterService(&AssetDepositService_ServiceDesc, srv) +} + +func _AssetDepositService_NewAssetDeposit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NewAssetDepositServerReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositServiceServer).NewAssetDeposit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositService/NewAssetDeposit", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositServiceServer).NewAssetDeposit(ctx, req.(*NewAssetDepositServerReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositService_WithdrawAssetDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WithdrawAssetDepositsServerReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositServiceServer).WithdrawAssetDeposits(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositService/WithdrawAssetDeposits", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositServiceServer).WithdrawAssetDeposits(ctx, req.(*WithdrawAssetDepositsServerReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositService_PushAssetDepositHtlcSigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PushAssetDepositHtlcSigsReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositServiceServer).PushAssetDepositHtlcSigs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositService/PushAssetDepositHtlcSigs", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositServiceServer).PushAssetDepositHtlcSigs(ctx, req.(*PushAssetDepositHtlcSigsReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositService_PushAssetDepositKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PushAssetDepositKeysReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositServiceServer).PushAssetDepositKeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositService/PushAssetDepositKeys", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositServiceServer).PushAssetDepositKeys(ctx, req.(*PushAssetDepositKeysReq)) + } + return interceptor(ctx, in, info, handler) +} + +// AssetDepositService_ServiceDesc is the grpc.ServiceDesc for AssetDepositService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AssetDepositService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "looprpc.AssetDepositService", + HandlerType: (*AssetDepositServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "NewAssetDeposit", + Handler: _AssetDepositService_NewAssetDeposit_Handler, + }, + { + MethodName: "WithdrawAssetDeposits", + Handler: _AssetDepositService_WithdrawAssetDeposits_Handler, + }, + { + MethodName: "PushAssetDepositHtlcSigs", + Handler: _AssetDepositService_PushAssetDepositHtlcSigs_Handler, + }, + { + MethodName: "PushAssetDepositKeys", + Handler: _AssetDepositService_PushAssetDepositKeys_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "asset_deposit.proto", +} From 9420ee6a6dc351f8d5f0e7387515639dd4cac84b Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:53:09 +0200 Subject: [PATCH 05/19] assets: add deposit kit encapsulating TAP deposit interactions This commit adds a high level deposit Kit type which includes the necessary functions to create and spend deposits to HTLC or through success, timeout or cooperative MuSig2 sweeps. --- assets/deposit/kit.go | 505 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 assets/deposit/kit.go diff --git a/assets/deposit/kit.go b/assets/deposit/kit.go new file mode 100644 index 000000000..b41a13ee1 --- /dev/null +++ b/assets/deposit/kit.go @@ -0,0 +1,505 @@ +package deposit + +import ( + "bytes" + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "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/loop/assets" + "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/rpcutils" + "github.com/lightninglabs/taproot-assets/tappsbt" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" +) + +// Kit is a struct that contains all the information needed to create +// and operator a 2-of-2 MuSig2 asset deposit. +type Kit struct { + // FunderScriptKey is a public key owned by the funder that acts as the + // script key for the deposit timeout sweep and as the key used when + // constructing the deposit spending zero fee HTLC. + FunderScriptKey *btcec.PublicKey + + // FunderInternalKey is a public key owned by the funder that is used + // for the derivation of the joint MuSig2 internal key of the deposit. + FunderInternalKey *btcec.PublicKey + + // CoSignerScriptKey is the script key of the counterparty who is + // co-signing the deposit funding and spending transactions. + CoSignerScriptKey *btcec.PublicKey + + // CoSignerKey is the internal key of the counterparty that is used for + // the derivation of the joint MuSig2 internal key of the deposit. + CoSignerInternalKey *btcec.PublicKey + + // KeyLocator is the locator of either the funder's or the co-signer's + // script key (depending on who is owning this Kit instance). If it is + // the funder's script key locator then it is used when signing a + // deposit timeout sweep transaction. + KeyLocator keychain.KeyLocator + + // AssetID is the identifier of the asset that will be held in the + // deposit. + AssetID asset.ID + + // CsvExpiry is the relative timelock in blocks for the timeout path of + // the deposit. + CsvExpiry uint32 + + // MuSig2Key is the aggregate key of the funder and co-signer. It is + // used as the internal key the output containing the deposit. + MuSig2Key *musig2.AggregateKey + + // chainParams is the chain parameters of the chain the deposit is + // being created on. + chainParams *address.ChainParams +} + +// NewKit creates a new deposit kit with the given funder key, co-signer +// key, key locator, asset ID and CSV expiry. +func NewKit(funderScriptKey, funderInternalKey, coSignerScriptKey, + coSignerInternalKey *btcec.PublicKey, keyLocator keychain.KeyLocator, + assetID asset.ID, csvExpiry uint32, chainParams *address.ChainParams) ( + *Kit, error) { + + sortKeys := true + muSig2Key, err := input.MuSig2CombineKeys( + input.MuSig2Version100RC2, + []*btcec.PublicKey{ + funderInternalKey, coSignerInternalKey, + }, + sortKeys, + &input.MuSig2Tweaks{ + TaprootBIP0086Tweak: true, + }, + ) + if err != nil { + return nil, err + } + + return &Kit{ + FunderScriptKey: funderScriptKey, + FunderInternalKey: funderInternalKey, + CoSignerScriptKey: coSignerScriptKey, + CoSignerInternalKey: coSignerInternalKey, + KeyLocator: keyLocator, + AssetID: assetID, + CsvExpiry: csvExpiry, + MuSig2Key: muSig2Key, + chainParams: chainParams, + }, nil +} + +// DeriveSharedDepositKey derives the internal key for the deposit from the +// passed public key by deriving a shared secret with the passed signer client. +func DeriveSharedDepositKey(ctx context.Context, + signer lndclient.SignerClient, pubKey *btcec.PublicKey) ( + *btcec.PublicKey, *btcec.PrivateKey, error) { + + secret, err := signer.DeriveSharedKey(ctx, pubKey, nil) + if err != nil { + return nil, nil, err + } + + derivedPrivKey, derivedPubKey := btcec.PrivKeyFromBytes(secret[:]) + + return derivedPubKey, derivedPrivKey, nil +} + +// GenTimeoutPathScript constructs a csv timeout script for the deposit funder. +// +// OP_CHECKSIGVERIFY OP_CHECKSEQUENCEVERIFY +func (d *Kit) GenTimeoutPathScript() ([]byte, error) { + builder := txscript.NewScriptBuilder() + + builder.AddData(schnorr.SerializePubKey(d.FunderScriptKey)) + builder.AddOp(txscript.OP_CHECKSIGVERIFY) + builder.AddInt64(int64(d.CsvExpiry)) + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + + return builder.Script() +} + +// genTimeoutPathSiblingPreimage generates the sibling preimage for the timeout +// path of the deposit. The sibling preimage is the preimage of the tap leaf +// that is the timeout path script. +func (d *Kit) genTimeoutPathSiblingPreimage() ([]byte, error) { + timeoutScript, err := d.GenTimeoutPathScript() + if err != nil { + return nil, err + } + + btcTapLeaf := txscript.TapLeaf{ + LeafVersion: txscript.BaseLeafVersion, + Script: timeoutScript, + } + + siblingPreimage, err := commitment.NewPreimageFromLeaf(btcTapLeaf) + if err != nil { + return nil, err + } + + preimageBytes, _, err := commitment.MaybeEncodeTapscriptPreimage( + siblingPreimage, + ) + if err != nil { + return nil, err + } + + return preimageBytes, nil +} + +// NewAddr creates a new deposit address to send funds to. The address is +// created with a MuSig2 key that is a combination of the funder and co-signer +// keys. The resulting anchor output will have a timeout path script that is a +// combination of the funder key and a CSV timelock. +func (d *Kit) NewAddr(ctx context.Context, funder *assets.TapdClient, + amount uint64) (*taprpc.Addr, error) { + + siblingPreimageBytes, err := d.genTimeoutPathSiblingPreimage() + if err != nil { + return nil, err + } + + tapScriptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return nil, err + } + + btcInternalKey := d.MuSig2Key.PreTweakedKey + muSig2Addr, err := funder.NewAddr(ctx, &taprpc.NewAddrRequest{ + AssetId: d.AssetID[:], + Amt: amount, + ScriptKey: rpcutils.MarshalScriptKey(tapScriptKey), + InternalKey: &taprpc.KeyDescriptor{ + RawKeyBytes: btcInternalKey.SerializeCompressed(), + }, + TapscriptSibling: siblingPreimageBytes, + }) + if err != nil { + return nil, err + } + + return muSig2Addr, nil +} + +// IsMatchingAddr checks if the given address is a matching deposit address for +// the deposit kit. It checks that the address has the same internal key, script +// key and sibling preimage as the deposit address. Note that this function does +// not check the amount of the address. +func (d *Kit) IsMatchingAddr(addr string) (bool, error) { + tap, err := address.DecodeAddress(addr, d.chainParams) + if err != nil { + return false, err + } + + tapSciptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return false, err + } + + keysMatch := tap.InternalKey.IsEqual(d.MuSig2Key.PreTweakedKey) && + tap.ScriptKey.IsEqual(tapSciptKey.PubKey) + + siblingPreimage1, err := d.genTimeoutPathSiblingPreimage() + if err != nil { + return false, err + } + + siblingPreimage2, _, err := commitment.MaybeEncodeTapscriptPreimage( + tap.TapscriptSibling, + ) + if err != nil { + return false, err + } + + return keysMatch && bytes.Equal(siblingPreimage1, siblingPreimage2), nil +} + +// GetMatchingOut checks if the given transfers contain a deposit output with +// the expected amount, script key and internal key. It returns the transfer +// and the index of the output if a match is found. If no match is found, it +// returns nil. +func (d *Kit) GetMatchingOut(amount uint64, transfers []*taprpc.AssetTransfer) ( + *taprpc.AssetTransfer, int, error) { + + // Prepare the tap scriptkey for the deposit. + tapSciptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return nil, 0, err + } + scriptKey := tapSciptKey.PubKey.SerializeCompressed() + scriptKey[0] = secp256k1.PubKeyFormatCompressedEven + + // Prepare the sibling preimage for the deposit. + siblingPreimage, err := d.genTimeoutPathSiblingPreimage() + if err != nil { + return nil, 0, err + } + + internalKey := d.MuSig2Key.PreTweakedKey.SerializeCompressed() + + // Now iterate over all the transfers to find the deposit. + for _, transfer := range transfers { + for outIndex, out := range transfer.Outputs { + // First make sure that the script key matches. + if !bytes.Equal(out.ScriptKey, scriptKey) { + continue + } + + // Make sure that the internal key also matches. + if !bytes.Equal(out.Anchor.InternalKey, internalKey) { + continue + } + + // Double check that the sibling preimage also matches. + if !bytes.Equal( + out.Anchor.TapscriptSibling, + siblingPreimage, + ) { + + continue + } + + // Make sure the amount is as expected. + if out.Amount == amount { + return transfer, outIndex, nil + } + } + } + + return nil, 0, nil +} + +// NewHtlcAddr creates a new HTLC address with the same keys as the deposit. +// This is useful when we're creating an HTLC transaction spending the deposit. +func (d *Kit) NewHtlcAddr(ctx context.Context, + tapClient *assets.TapdClient, amount uint64, swapHash lntypes.Hash, + csvExpiry uint32) (*taprpc.Addr, *htlc.SwapKit, error) { + + s := htlc.SwapKit{ + SenderPubKey: d.FunderScriptKey, + ReceiverPubKey: d.CoSignerScriptKey, + AssetID: d.AssetID[:], + Amount: btcutil.Amount(amount), + SwapHash: swapHash, + CsvExpiry: csvExpiry, + } + + btcInternalKey, err := s.GetAggregateKey() + if err != nil { + return nil, nil, err + } + + siblingPreimage, err := s.GetSiblingPreimage() + if err != nil { + return nil, nil, err + } + + siblingPreimageBytes, _, err := commitment.MaybeEncodeTapscriptPreimage( + &siblingPreimage, + ) + if err != nil { + return nil, nil, err + } + + tapScriptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return nil, nil, err + } + + htlcAddr, err := tapClient.NewAddr(ctx, &taprpc.NewAddrRequest{ + AssetId: d.AssetID[:], + Amt: amount, + ScriptKey: rpcutils.MarshalScriptKey(tapScriptKey), + InternalKey: &taprpc.KeyDescriptor{ + RawKeyBytes: btcInternalKey.SerializeCompressed(), + }, + TapscriptSibling: siblingPreimageBytes, + }) + if err != nil { + return nil, nil, err + } + + return htlcAddr, &s, nil +} + +// TapScriptKey generates a TAP script-key (the key of the script locking the +// asset) for the deposit. +func (d *Kit) TapScriptKey() (asset.ScriptKey, error) { + tapScriptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return asset.ScriptKey{}, err + } + + return asset.NewScriptKey(tapScriptKey.PubKey), nil +} + +// ExportProof exports a proof for the deposit outpoint. The proof is used to +// prove that the deposit is valid and indeed happened. +func (d *Kit) ExportProof(ctx context.Context, funder *assets.TapdClient, + outpoint *wire.OutPoint) (*taprpc.ProofFile, error) { + + scriptKey, err := d.TapScriptKey() + if err != nil { + return nil, err + } + + return funder.ExportProof( + ctx, &taprpc.ExportProofRequest{ + AssetId: d.AssetID[:], + ScriptKey: scriptKey.PubKey.SerializeCompressed(), + Outpoint: &taprpc.OutPoint{ + Txid: outpoint.Hash[:], + OutputIndex: outpoint.Index, + }, + }, + ) +} + +// VerifyProof verifies that the given deposit proof is valid for the deposit +// address. It checks that the internal key of the proof matches the internal +// key of the deposit address and that the sibling preimage of the proof matches +// the sibling preimage of the deposit address. Returns the root hash of the +// anchor output if the proof is valid. +func (d *Kit) VerifyProof(depositProof *proof.Proof) ([]byte, error) { + // First generate a vpacket from the deposit proof. + proofVpacket, err := tappsbt.FromProofs( + []*proof.Proof{depositProof}, d.chainParams, tappsbt.V1, + ) + if err != nil { + return nil, err + } + + // Now verify that the proof is indeed for the deposit address. + input := proofVpacket.Inputs[0] + + // First check that the internal key of the proof matches the internal + // key of the deposit address. + anchorInternalKeyBytes := input.Anchor.InternalKey.SerializeCompressed() + depositInternalKey := d.MuSig2Key.PreTweakedKey.SerializeCompressed() + + if !bytes.Equal(depositInternalKey, anchorInternalKeyBytes) { + return nil, fmt.Errorf("VerifyProof: internal key mismatch") + } + + // Next check that the sibling preimage of the proof matches the sibling + // preimage of the deposit address. + depositSiblingPreimage, err := d.genTimeoutPathSiblingPreimage() + if err != nil { + return nil, err + } + + if !bytes.Equal(depositSiblingPreimage, input.Anchor.TapscriptSibling) { + return nil, fmt.Errorf("VerifyProof: sibling preimage mismatch") + } + + return input.Anchor.MerkleRoot, nil +} + +// GenTimeoutBtcControlBlock generates the control block for the timeout path of +// the deposit. +func (d *Kit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) ( + *txscript.ControlBlock, error) { + + internalKey := d.MuSig2Key.PreTweakedKey + + btcControlBlock := &txscript.ControlBlock{ + InternalKey: internalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: taprootAssetRoot, + } + + timeoutPathScript, err := d.GenTimeoutPathScript() + 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 +} + +// CreateTimeoutWitness creates a timeout witness for the deposit. +func (d *Kit) CreateTimeoutWitness(ctx context.Context, + signer lndclient.SignerClient, depositProof *proof.Proof, + sweepBtcPacket *psbt.Packet) (wire.TxWitness, error) { + + assetTxOut := sweepBtcPacket.Inputs[0].WitnessUtxo + feeTxOut := sweepBtcPacket.Inputs[1].WitnessUtxo + sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = d.CsvExpiry + + timeoutScript, err := d.GenTimeoutPathScript() + if err != nil { + return nil, err + } + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: d.KeyLocator, + }, + SignMethod: input.TaprootScriptSpendSignMethod, + WitnessScript: timeoutScript, + Output: assetTxOut, + InputIndex: 0, + } + rawSigs, err := signer.SignOutputRaw( + ctx, sweepBtcPacket.UnsignedTx, + []*lndclient.SignDescriptor{ + signDesc, + }, + []*wire.TxOut{ + assetTxOut, feeTxOut, + }, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot, err := assets.GenTaprootAssetRootFromProof( + depositProof, + ) + if err != nil { + return nil, err + } + + timeoutControlBlock, err := d.GenTimeoutBtcControlBlock( + taprootAssetRoot, + ) + if err != nil { + return nil, err + } + + controlBlockBytes, err := timeoutControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + rawSigs[0], + timeoutScript, + controlBlockBytes, + }, nil +} From 2d516e9285254a3caf975b762afdfd008c0f1150 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 09:21:02 +0200 Subject: [PATCH 06/19] looprpc: add client side asset deposit service proto stubs This commit adds protos for the loopd asset deposit subserver RPCs. --- looprpc/client_asset_deposit.pb.go | 921 ++++++++++++++++++++++++ looprpc/client_asset_deposit.proto | 92 +++ looprpc/client_asset_deposit_grpc.pb.go | 245 +++++++ looprpc/perms.go | 20 + 4 files changed, 1278 insertions(+) create mode 100644 looprpc/client_asset_deposit.pb.go create mode 100644 looprpc/client_asset_deposit.proto create mode 100644 looprpc/client_asset_deposit_grpc.pb.go diff --git a/looprpc/client_asset_deposit.pb.go b/looprpc/client_asset_deposit.pb.go new file mode 100644 index 000000000..139e08019 --- /dev/null +++ b/looprpc/client_asset_deposit.pb.go @@ -0,0 +1,921 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.2 +// protoc v3.21.12 +// source: client_asset_deposit.proto + +package looprpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NewAssetDepositRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AssetId string `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` + CsvExpiry int32 `protobuf:"varint,3,opt,name=csv_expiry,json=csvExpiry,proto3" json:"csv_expiry,omitempty"` +} + +func (x *NewAssetDepositRequest) Reset() { + *x = NewAssetDepositRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewAssetDepositRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewAssetDepositRequest) ProtoMessage() {} + +func (x *NewAssetDepositRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewAssetDepositRequest.ProtoReflect.Descriptor instead. +func (*NewAssetDepositRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{0} +} + +func (x *NewAssetDepositRequest) GetAssetId() string { + if x != nil { + return x.AssetId + } + return "" +} + +func (x *NewAssetDepositRequest) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *NewAssetDepositRequest) GetCsvExpiry() int32 { + if x != nil { + return x.CsvExpiry + } + return 0 +} + +type NewAssetDepositResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` +} + +func (x *NewAssetDepositResponse) Reset() { + *x = NewAssetDepositResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NewAssetDepositResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewAssetDepositResponse) ProtoMessage() {} + +func (x *NewAssetDepositResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewAssetDepositResponse.ProtoReflect.Descriptor instead. +func (*NewAssetDepositResponse) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{1} +} + +func (x *NewAssetDepositResponse) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +type ListAssetDepositsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The number of minimum confirmations a deposit anchor must have to be + // listed. + MinConfs uint32 `protobuf:"varint,1,opt,name=min_confs,json=minConfs,proto3" json:"min_confs,omitempty"` + // The number of maximum confirmations a deposit anchor may have to be + // listed. A zero value indicates that there is no maximum. + MaxConfs uint32 `protobuf:"varint,2,opt,name=max_confs,json=maxConfs,proto3" json:"max_confs,omitempty"` +} + +func (x *ListAssetDepositsRequest) Reset() { + *x = ListAssetDepositsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAssetDepositsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAssetDepositsRequest) ProtoMessage() {} + +func (x *ListAssetDepositsRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAssetDepositsRequest.ProtoReflect.Descriptor instead. +func (*ListAssetDepositsRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{2} +} + +func (x *ListAssetDepositsRequest) GetMinConfs() uint32 { + if x != nil { + return x.MinConfs + } + return 0 +} + +func (x *ListAssetDepositsRequest) GetMaxConfs() uint32 { + if x != nil { + return x.MaxConfs + } + return 0 +} + +type ListAssetDepositsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // A list of all deposits that match the filtered state. + FilteredDeposits []*AssetDeposit `protobuf:"bytes,1,rep,name=filtered_deposits,json=filteredDeposits,proto3" json:"filtered_deposits,omitempty"` +} + +func (x *ListAssetDepositsResponse) Reset() { + *x = ListAssetDepositsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListAssetDepositsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAssetDepositsResponse) ProtoMessage() {} + +func (x *ListAssetDepositsResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAssetDepositsResponse.ProtoReflect.Descriptor instead. +func (*ListAssetDepositsResponse) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{3} +} + +func (x *ListAssetDepositsResponse) GetFilteredDeposits() []*AssetDeposit { + if x != nil { + return x.FilteredDeposits + } + return nil +} + +type AssetDeposit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` + CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + AssetId string `protobuf:"bytes,3,opt,name=asset_id,json=assetId,proto3" json:"asset_id,omitempty"` + Amount uint64 `protobuf:"varint,4,opt,name=amount,proto3" json:"amount,omitempty"` + DepositAddr string `protobuf:"bytes,5,opt,name=deposit_addr,json=depositAddr,proto3" json:"deposit_addr,omitempty"` + State string `protobuf:"bytes,6,opt,name=state,proto3" json:"state,omitempty"` + AnchorOutpoint string `protobuf:"bytes,7,opt,name=anchor_outpoint,json=anchorOutpoint,proto3" json:"anchor_outpoint,omitempty"` + ConfirmationHeight uint32 `protobuf:"varint,8,opt,name=confirmation_height,json=confirmationHeight,proto3" json:"confirmation_height,omitempty"` + Expiry uint32 `protobuf:"varint,9,opt,name=expiry,proto3" json:"expiry,omitempty"` + SweepAddr string `protobuf:"bytes,10,opt,name=sweep_addr,json=sweepAddr,proto3" json:"sweep_addr,omitempty"` +} + +func (x *AssetDeposit) Reset() { + *x = AssetDeposit{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AssetDeposit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetDeposit) ProtoMessage() {} + +func (x *AssetDeposit) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AssetDeposit.ProtoReflect.Descriptor instead. +func (*AssetDeposit) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{4} +} + +func (x *AssetDeposit) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +func (x *AssetDeposit) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *AssetDeposit) GetAssetId() string { + if x != nil { + return x.AssetId + } + return "" +} + +func (x *AssetDeposit) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +func (x *AssetDeposit) GetDepositAddr() string { + if x != nil { + return x.DepositAddr + } + return "" +} + +func (x *AssetDeposit) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *AssetDeposit) GetAnchorOutpoint() string { + if x != nil { + return x.AnchorOutpoint + } + return "" +} + +func (x *AssetDeposit) GetConfirmationHeight() uint32 { + if x != nil { + return x.ConfirmationHeight + } + return 0 +} + +func (x *AssetDeposit) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +func (x *AssetDeposit) GetSweepAddr() string { + if x != nil { + return x.SweepAddr + } + return "" +} + +type WithdrawAssetDepositsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositIds []string `protobuf:"bytes,1,rep,name=deposit_ids,json=depositIds,proto3" json:"deposit_ids,omitempty"` +} + +func (x *WithdrawAssetDepositsRequest) Reset() { + *x = WithdrawAssetDepositsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WithdrawAssetDepositsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WithdrawAssetDepositsRequest) ProtoMessage() {} + +func (x *WithdrawAssetDepositsRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WithdrawAssetDepositsRequest.ProtoReflect.Descriptor instead. +func (*WithdrawAssetDepositsRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{5} +} + +func (x *WithdrawAssetDepositsRequest) GetDepositIds() []string { + if x != nil { + return x.DepositIds + } + return nil +} + +type WithdrawAssetDepositsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *WithdrawAssetDepositsResponse) Reset() { + *x = WithdrawAssetDepositsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WithdrawAssetDepositsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WithdrawAssetDepositsResponse) ProtoMessage() {} + +func (x *WithdrawAssetDepositsResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WithdrawAssetDepositsResponse.ProtoReflect.Descriptor instead. +func (*WithdrawAssetDepositsResponse) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{6} +} + +type RevealAssetDepositKeyRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` +} + +func (x *RevealAssetDepositKeyRequest) Reset() { + *x = RevealAssetDepositKeyRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevealAssetDepositKeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevealAssetDepositKeyRequest) ProtoMessage() {} + +func (x *RevealAssetDepositKeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevealAssetDepositKeyRequest.ProtoReflect.Descriptor instead. +func (*RevealAssetDepositKeyRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{7} +} + +func (x *RevealAssetDepositKeyRequest) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +type RevealAssetDepositKeyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RevealAssetDepositKeyResponse) Reset() { + *x = RevealAssetDepositKeyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RevealAssetDepositKeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevealAssetDepositKeyResponse) ProtoMessage() {} + +func (x *RevealAssetDepositKeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevealAssetDepositKeyResponse.ProtoReflect.Descriptor instead. +func (*RevealAssetDepositKeyResponse) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{8} +} + +type TestCoSignAssetDepositHTLCRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DepositId string `protobuf:"bytes,1,opt,name=deposit_id,json=depositId,proto3" json:"deposit_id,omitempty"` +} + +func (x *TestCoSignAssetDepositHTLCRequest) Reset() { + *x = TestCoSignAssetDepositHTLCRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestCoSignAssetDepositHTLCRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestCoSignAssetDepositHTLCRequest) ProtoMessage() {} + +func (x *TestCoSignAssetDepositHTLCRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestCoSignAssetDepositHTLCRequest.ProtoReflect.Descriptor instead. +func (*TestCoSignAssetDepositHTLCRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{9} +} + +func (x *TestCoSignAssetDepositHTLCRequest) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +type TestCoSignAssetDepositHTLCResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TestCoSignAssetDepositHTLCResponse) Reset() { + *x = TestCoSignAssetDepositHTLCResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TestCoSignAssetDepositHTLCResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TestCoSignAssetDepositHTLCResponse) ProtoMessage() {} + +func (x *TestCoSignAssetDepositHTLCResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_asset_deposit_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TestCoSignAssetDepositHTLCResponse.ProtoReflect.Descriptor instead. +func (*TestCoSignAssetDepositHTLCResponse) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{10} +} + +var File_client_asset_deposit_proto protoreflect.FileDescriptor + +var file_client_asset_deposit_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x64, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0x6a, 0x0a, 0x16, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x63, 0x73, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x22, 0x38, 0x0a, 0x17, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, 0x64, 0x22, 0x54, 0x0a, 0x18, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x43, + 0x6f, 0x6e, 0x66, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x63, 0x6f, 0x6e, 0x66, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x43, 0x6f, 0x6e, 0x66, + 0x73, 0x22, 0x5f, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, + 0x0a, 0x11, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x52, 0x10, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x65, 0x64, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x73, 0x22, 0xc9, 0x02, 0x0a, 0x0c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, + 0x0f, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x4f, 0x75, + 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, + 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x12, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, + 0x1d, 0x0a, 0x0a, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x77, 0x65, 0x65, 0x70, 0x41, 0x64, 0x64, 0x72, 0x22, 0x3f, + 0x0a, 0x1c, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, + 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, 0x64, 0x73, 0x22, + 0x1f, 0x0a, 0x1d, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x3d, 0x0a, 0x1c, 0x52, 0x65, 0x76, 0x65, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x49, 0x64, 0x22, + 0x1f, 0x0a, 0x1d, 0x52, 0x65, 0x76, 0x65, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x42, 0x0a, 0x21, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x54, 0x4c, 0x43, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x49, 0x64, 0x22, 0x24, 0x0a, 0x22, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x53, 0x69, + 0x67, 0x6e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x54, + 0x4c, 0x43, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x8d, 0x04, 0x0a, 0x12, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x12, 0x54, 0x0a, 0x0f, 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x12, 0x1f, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4e, + 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x4e, 0x65, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x12, 0x21, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x22, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x15, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x6c, + 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x69, 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x57, 0x69, + 0x74, 0x68, 0x64, 0x72, 0x61, 0x77, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x15, 0x52, + 0x65, 0x76, 0x65, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x4b, 0x65, 0x79, 0x12, 0x25, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, + 0x65, 0x76, 0x65, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x61, 0x6c, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x1a, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x53, 0x69, 0x67, + 0x6e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x54, 0x4c, + 0x43, 0x12, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x73, 0x74, + 0x43, 0x6f, 0x53, 0x69, 0x67, 0x6e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x48, 0x54, 0x4c, 0x43, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, + 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x53, 0x69, + 0x67, 0x6e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x54, + 0x4c, 0x43, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, + 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_client_asset_deposit_proto_rawDescOnce sync.Once + file_client_asset_deposit_proto_rawDescData = file_client_asset_deposit_proto_rawDesc +) + +func file_client_asset_deposit_proto_rawDescGZIP() []byte { + file_client_asset_deposit_proto_rawDescOnce.Do(func() { + file_client_asset_deposit_proto_rawDescData = protoimpl.X.CompressGZIP(file_client_asset_deposit_proto_rawDescData) + }) + return file_client_asset_deposit_proto_rawDescData +} + +var file_client_asset_deposit_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_client_asset_deposit_proto_goTypes = []any{ + (*NewAssetDepositRequest)(nil), // 0: looprpc.NewAssetDepositRequest + (*NewAssetDepositResponse)(nil), // 1: looprpc.NewAssetDepositResponse + (*ListAssetDepositsRequest)(nil), // 2: looprpc.ListAssetDepositsRequest + (*ListAssetDepositsResponse)(nil), // 3: looprpc.ListAssetDepositsResponse + (*AssetDeposit)(nil), // 4: looprpc.AssetDeposit + (*WithdrawAssetDepositsRequest)(nil), // 5: looprpc.WithdrawAssetDepositsRequest + (*WithdrawAssetDepositsResponse)(nil), // 6: looprpc.WithdrawAssetDepositsResponse + (*RevealAssetDepositKeyRequest)(nil), // 7: looprpc.RevealAssetDepositKeyRequest + (*RevealAssetDepositKeyResponse)(nil), // 8: looprpc.RevealAssetDepositKeyResponse + (*TestCoSignAssetDepositHTLCRequest)(nil), // 9: looprpc.TestCoSignAssetDepositHTLCRequest + (*TestCoSignAssetDepositHTLCResponse)(nil), // 10: looprpc.TestCoSignAssetDepositHTLCResponse +} +var file_client_asset_deposit_proto_depIdxs = []int32{ + 4, // 0: looprpc.ListAssetDepositsResponse.filtered_deposits:type_name -> looprpc.AssetDeposit + 0, // 1: looprpc.AssetDepositClient.NewAssetDeposit:input_type -> looprpc.NewAssetDepositRequest + 2, // 2: looprpc.AssetDepositClient.ListAssetDeposits:input_type -> looprpc.ListAssetDepositsRequest + 5, // 3: looprpc.AssetDepositClient.WithdrawAssetDeposits:input_type -> looprpc.WithdrawAssetDepositsRequest + 7, // 4: looprpc.AssetDepositClient.RevealAssetDepositKey:input_type -> looprpc.RevealAssetDepositKeyRequest + 9, // 5: looprpc.AssetDepositClient.TestCoSignAssetDepositHTLC:input_type -> looprpc.TestCoSignAssetDepositHTLCRequest + 1, // 6: looprpc.AssetDepositClient.NewAssetDeposit:output_type -> looprpc.NewAssetDepositResponse + 3, // 7: looprpc.AssetDepositClient.ListAssetDeposits:output_type -> looprpc.ListAssetDepositsResponse + 6, // 8: looprpc.AssetDepositClient.WithdrawAssetDeposits:output_type -> looprpc.WithdrawAssetDepositsResponse + 8, // 9: looprpc.AssetDepositClient.RevealAssetDepositKey:output_type -> looprpc.RevealAssetDepositKeyResponse + 10, // 10: looprpc.AssetDepositClient.TestCoSignAssetDepositHTLC:output_type -> looprpc.TestCoSignAssetDepositHTLCResponse + 6, // [6:11] is the sub-list for method output_type + 1, // [1:6] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_client_asset_deposit_proto_init() } +func file_client_asset_deposit_proto_init() { + if File_client_asset_deposit_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_client_asset_deposit_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*NewAssetDepositRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*NewAssetDepositResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*ListAssetDepositsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[3].Exporter = func(v any, i int) any { + switch v := v.(*ListAssetDepositsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[4].Exporter = func(v any, i int) any { + switch v := v.(*AssetDeposit); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[5].Exporter = func(v any, i int) any { + switch v := v.(*WithdrawAssetDepositsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*WithdrawAssetDepositsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*RevealAssetDepositKeyRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*RevealAssetDepositKeyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*TestCoSignAssetDepositHTLCRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_client_asset_deposit_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*TestCoSignAssetDepositHTLCResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_client_asset_deposit_proto_rawDesc, + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_client_asset_deposit_proto_goTypes, + DependencyIndexes: file_client_asset_deposit_proto_depIdxs, + MessageInfos: file_client_asset_deposit_proto_msgTypes, + }.Build() + File_client_asset_deposit_proto = out.File + file_client_asset_deposit_proto_rawDesc = nil + file_client_asset_deposit_proto_goTypes = nil + file_client_asset_deposit_proto_depIdxs = nil +} diff --git a/looprpc/client_asset_deposit.proto b/looprpc/client_asset_deposit.proto new file mode 100644 index 000000000..b86caaef0 --- /dev/null +++ b/looprpc/client_asset_deposit.proto @@ -0,0 +1,92 @@ +syntax = "proto3"; + +package looprpc; + +option go_package = "github.com/lightninglabs/loop/looprpc"; + +service AssetDepositClient { + rpc NewAssetDeposit (NewAssetDepositRequest) + returns (NewAssetDepositResponse); + + rpc ListAssetDeposits (ListAssetDepositsRequest) + returns (ListAssetDepositsResponse); + + rpc WithdrawAssetDeposits (WithdrawAssetDepositsRequest) + returns (WithdrawAssetDepositsResponse); + + rpc RevealAssetDepositKey (RevealAssetDepositKeyRequest) + returns (RevealAssetDepositKeyResponse); + + rpc TestCoSignAssetDepositHTLC (TestCoSignAssetDepositHTLCRequest) + returns (TestCoSignAssetDepositHTLCResponse); +} + +message NewAssetDepositRequest { + string asset_id = 1; + + uint64 amount = 2; + + int32 csv_expiry = 3; +} + +message NewAssetDepositResponse { + string deposit_id = 1; +} + +message ListAssetDepositsRequest { + // The number of minimum confirmations a deposit anchor must have to be + // listed. + uint32 min_confs = 1; + + // The number of maximum confirmations a deposit anchor may have to be + // listed. A zero value indicates that there is no maximum. + uint32 max_confs = 2; +} + +message ListAssetDepositsResponse { + // A list of all deposits that match the filtered state. + repeated AssetDeposit filtered_deposits = 1; +} + +message AssetDeposit { + string deposit_id = 1; + + int64 created_at = 2; + + string asset_id = 3; + + uint64 amount = 4; + + string deposit_addr = 5; + + string state = 6; + + string anchor_outpoint = 7; + + uint32 confirmation_height = 8; + + uint32 expiry = 9; + + string sweep_addr = 10; +} + +message WithdrawAssetDepositsRequest { + repeated string deposit_ids = 1; +} + +message WithdrawAssetDepositsResponse { +} + +message RevealAssetDepositKeyRequest { + string deposit_id = 1; +} + +message RevealAssetDepositKeyResponse { +} + +message TestCoSignAssetDepositHTLCRequest { + string deposit_id = 1; +} + +message TestCoSignAssetDepositHTLCResponse { +} diff --git a/looprpc/client_asset_deposit_grpc.pb.go b/looprpc/client_asset_deposit_grpc.pb.go new file mode 100644 index 000000000..0101d6b75 --- /dev/null +++ b/looprpc/client_asset_deposit_grpc.pb.go @@ -0,0 +1,245 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package looprpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// AssetDepositClientClient is the client API for AssetDepositClient service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AssetDepositClientClient interface { + NewAssetDeposit(ctx context.Context, in *NewAssetDepositRequest, opts ...grpc.CallOption) (*NewAssetDepositResponse, error) + ListAssetDeposits(ctx context.Context, in *ListAssetDepositsRequest, opts ...grpc.CallOption) (*ListAssetDepositsResponse, error) + WithdrawAssetDeposits(ctx context.Context, in *WithdrawAssetDepositsRequest, opts ...grpc.CallOption) (*WithdrawAssetDepositsResponse, error) + RevealAssetDepositKey(ctx context.Context, in *RevealAssetDepositKeyRequest, opts ...grpc.CallOption) (*RevealAssetDepositKeyResponse, error) + TestCoSignAssetDepositHTLC(ctx context.Context, in *TestCoSignAssetDepositHTLCRequest, opts ...grpc.CallOption) (*TestCoSignAssetDepositHTLCResponse, error) +} + +type assetDepositClientClient struct { + cc grpc.ClientConnInterface +} + +func NewAssetDepositClientClient(cc grpc.ClientConnInterface) AssetDepositClientClient { + return &assetDepositClientClient{cc} +} + +func (c *assetDepositClientClient) NewAssetDeposit(ctx context.Context, in *NewAssetDepositRequest, opts ...grpc.CallOption) (*NewAssetDepositResponse, error) { + out := new(NewAssetDepositResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/NewAssetDeposit", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositClientClient) ListAssetDeposits(ctx context.Context, in *ListAssetDepositsRequest, opts ...grpc.CallOption) (*ListAssetDepositsResponse, error) { + out := new(ListAssetDepositsResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/ListAssetDeposits", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositClientClient) WithdrawAssetDeposits(ctx context.Context, in *WithdrawAssetDepositsRequest, opts ...grpc.CallOption) (*WithdrawAssetDepositsResponse, error) { + out := new(WithdrawAssetDepositsResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/WithdrawAssetDeposits", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositClientClient) RevealAssetDepositKey(ctx context.Context, in *RevealAssetDepositKeyRequest, opts ...grpc.CallOption) (*RevealAssetDepositKeyResponse, error) { + out := new(RevealAssetDepositKeyResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/RevealAssetDepositKey", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *assetDepositClientClient) TestCoSignAssetDepositHTLC(ctx context.Context, in *TestCoSignAssetDepositHTLCRequest, opts ...grpc.CallOption) (*TestCoSignAssetDepositHTLCResponse, error) { + out := new(TestCoSignAssetDepositHTLCResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/TestCoSignAssetDepositHTLC", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AssetDepositClientServer is the server API for AssetDepositClient service. +// All implementations must embed UnimplementedAssetDepositClientServer +// for forward compatibility +type AssetDepositClientServer interface { + NewAssetDeposit(context.Context, *NewAssetDepositRequest) (*NewAssetDepositResponse, error) + ListAssetDeposits(context.Context, *ListAssetDepositsRequest) (*ListAssetDepositsResponse, error) + WithdrawAssetDeposits(context.Context, *WithdrawAssetDepositsRequest) (*WithdrawAssetDepositsResponse, error) + RevealAssetDepositKey(context.Context, *RevealAssetDepositKeyRequest) (*RevealAssetDepositKeyResponse, error) + TestCoSignAssetDepositHTLC(context.Context, *TestCoSignAssetDepositHTLCRequest) (*TestCoSignAssetDepositHTLCResponse, error) + mustEmbedUnimplementedAssetDepositClientServer() +} + +// UnimplementedAssetDepositClientServer must be embedded to have forward compatible implementations. +type UnimplementedAssetDepositClientServer struct { +} + +func (UnimplementedAssetDepositClientServer) NewAssetDeposit(context.Context, *NewAssetDepositRequest) (*NewAssetDepositResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method NewAssetDeposit not implemented") +} +func (UnimplementedAssetDepositClientServer) ListAssetDeposits(context.Context, *ListAssetDepositsRequest) (*ListAssetDepositsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAssetDeposits not implemented") +} +func (UnimplementedAssetDepositClientServer) WithdrawAssetDeposits(context.Context, *WithdrawAssetDepositsRequest) (*WithdrawAssetDepositsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method WithdrawAssetDeposits not implemented") +} +func (UnimplementedAssetDepositClientServer) RevealAssetDepositKey(context.Context, *RevealAssetDepositKeyRequest) (*RevealAssetDepositKeyResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevealAssetDepositKey not implemented") +} +func (UnimplementedAssetDepositClientServer) TestCoSignAssetDepositHTLC(context.Context, *TestCoSignAssetDepositHTLCRequest) (*TestCoSignAssetDepositHTLCResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TestCoSignAssetDepositHTLC not implemented") +} +func (UnimplementedAssetDepositClientServer) mustEmbedUnimplementedAssetDepositClientServer() {} + +// UnsafeAssetDepositClientServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AssetDepositClientServer will +// result in compilation errors. +type UnsafeAssetDepositClientServer interface { + mustEmbedUnimplementedAssetDepositClientServer() +} + +func RegisterAssetDepositClientServer(s grpc.ServiceRegistrar, srv AssetDepositClientServer) { + s.RegisterService(&AssetDepositClient_ServiceDesc, srv) +} + +func _AssetDepositClient_NewAssetDeposit_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NewAssetDepositRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).NewAssetDeposit(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/NewAssetDeposit", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).NewAssetDeposit(ctx, req.(*NewAssetDepositRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositClient_ListAssetDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAssetDepositsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).ListAssetDeposits(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/ListAssetDeposits", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).ListAssetDeposits(ctx, req.(*ListAssetDepositsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositClient_WithdrawAssetDeposits_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(WithdrawAssetDepositsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).WithdrawAssetDeposits(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/WithdrawAssetDeposits", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).WithdrawAssetDeposits(ctx, req.(*WithdrawAssetDepositsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositClient_RevealAssetDepositKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevealAssetDepositKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).RevealAssetDepositKey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/RevealAssetDepositKey", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).RevealAssetDepositKey(ctx, req.(*RevealAssetDepositKeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AssetDepositClient_TestCoSignAssetDepositHTLC_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestCoSignAssetDepositHTLCRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).TestCoSignAssetDepositHTLC(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/TestCoSignAssetDepositHTLC", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).TestCoSignAssetDepositHTLC(ctx, req.(*TestCoSignAssetDepositHTLCRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AssetDepositClient_ServiceDesc is the grpc.ServiceDesc for AssetDepositClient service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AssetDepositClient_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "looprpc.AssetDepositClient", + HandlerType: (*AssetDepositClientServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "NewAssetDeposit", + Handler: _AssetDepositClient_NewAssetDeposit_Handler, + }, + { + MethodName: "ListAssetDeposits", + Handler: _AssetDepositClient_ListAssetDeposits_Handler, + }, + { + MethodName: "WithdrawAssetDeposits", + Handler: _AssetDepositClient_WithdrawAssetDeposits_Handler, + }, + { + MethodName: "RevealAssetDepositKey", + Handler: _AssetDepositClient_RevealAssetDepositKey_Handler, + }, + { + MethodName: "TestCoSignAssetDepositHTLC", + Handler: _AssetDepositClient_TestCoSignAssetDepositHTLC_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "client_asset_deposit.proto", +} diff --git a/looprpc/perms.go b/looprpc/perms.go index be28d7c8d..573f5d4b6 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -176,4 +176,24 @@ var RequiredPermissions = map[string][]bakery.Op{ Entity: "swap", Action: "read", }}, + "/looprpc.AssetDepositClient/NewAssetDeposit": {{ + Entity: "swap", + Action: "execute", + }}, + "/looprpc.AssetDepositClient/ListAssetDeposits": {{ + Entity: "swap", + Action: "read", + }}, + "/looprpc.AssetDepositClient/WithdrawAssetDeposits": {{ + Entity: "swap", + Action: "read", + }}, + "/looprpc.AssetDepositClient/CoSignAssetDepositHTLC": {{ + Entity: "swap", + Action: "execute", + }}, + "/looprpc.AssetDepositClient/RevealAssetDepositKey": {{ + Entity: "swap", + Action: "execute", + }}, } From 204a98ef31c8831abdedd93d5c970d18737ef9bf Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:43:51 +0200 Subject: [PATCH 07/19] loop: add stubs and CLI commands for the asset deposit subserver This commit adds a placeholder asset deposit subserver along with the corresponding CLI commands to the Loop daemon and CLI. --- assets/deposit/log.go | 26 ++++ assets/deposit/server.go | 63 +++++++++ cmd/loop/asset_deposits.go | 255 +++++++++++++++++++++++++++++++++++++ cmd/loop/main.go | 24 +++- loopd/daemon.go | 11 ++ loopd/log.go | 4 + loopd/swapclient_server.go | 2 + 7 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 assets/deposit/log.go create mode 100644 assets/deposit/server.go create mode 100644 cmd/loop/asset_deposits.go diff --git a/assets/deposit/log.go b/assets/deposit/log.go new file mode 100644 index 000000000..b9463ce46 --- /dev/null +++ b/assets/deposit/log.go @@ -0,0 +1,26 @@ +package deposit + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "ADEP" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go new file mode 100644 index 000000000..8e3f3b43d --- /dev/null +++ b/assets/deposit/server.go @@ -0,0 +1,63 @@ +package deposit + +import ( + "context" + + "github.com/lightninglabs/loop/looprpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Server is the grpc server that serves the reservation service. +type Server struct { + looprpc.UnimplementedAssetDepositClientServer +} + +func NewServer() *Server { + return &Server{} +} + +// NewAssetDeposit is the rpc endpoint for loop clients to request a new asset +// deposit. +func (s *Server) NewAssetDeposit(ctx context.Context, + in *looprpc.NewAssetDepositRequest) (*looprpc.NewAssetDepositResponse, + error) { + + return nil, status.Error(codes.Unimplemented, "unimplemented") +} + +// ListAssetDeposits is the rpc endpoint for loop clients to list their asset +// deposits. +func (s *Server) ListAssetDeposits(ctx context.Context, + in *looprpc.ListAssetDepositsRequest) ( + *looprpc.ListAssetDepositsResponse, error) { + + return nil, status.Error(codes.Unimplemented, "unimplemented") +} + +// RevealAssetDepositKey is the rpc endpoint for loop clients to reveal the +// asset deposit key for a specific asset deposit. +func (s *Server) RevealAssetDepositKey(ctx context.Context, + in *looprpc.RevealAssetDepositKeyRequest) ( + *looprpc.RevealAssetDepositKeyResponse, error) { + + return nil, status.Error(codes.Unimplemented, "unimplemented") +} + +// WithdrawAssetDeposits is the rpc endpoint for loop clients to withdraw their +// asset deposits. +func (s *Server) WithdrawAssetDeposits(ctx context.Context, + in *looprpc.WithdrawAssetDepositsRequest) ( + *looprpc.WithdrawAssetDepositsResponse, error) { + + return nil, status.Error(codes.Unimplemented, "unimplemented") +} + +// TestCoSignAssetDepositHTLC is the rpc endpoint for loop clients to test +// co-signing an asset deposit HTLC. +func (s *Server) TestCoSignAssetDepositHTLC(ctx context.Context, + in *looprpc.TestCoSignAssetDepositHTLCRequest) ( + *looprpc.TestCoSignAssetDepositHTLCResponse, error) { + + return nil, status.Error(codes.Unimplemented, "unimplemented") +} diff --git a/cmd/loop/asset_deposits.go b/cmd/loop/asset_deposits.go new file mode 100644 index 000000000..5538b1ab3 --- /dev/null +++ b/cmd/loop/asset_deposits.go @@ -0,0 +1,255 @@ +package main + +import ( + "context" + + "github.com/lightninglabs/loop/looprpc" + "github.com/urfave/cli" +) + +var ( + assetDepositsCommands = cli.Command{ + Name: "asset deposits", + ShortName: "ad", + Usage: "TAP asset deposit commands.", + Subcommands: []cli.Command{ + newAssetDepositCommand, + listAssetDepositsCommand, + withdrawAssetDepositCommand, + testCoSignCommand, + testKeyRevealCommand, + }, + } + + newAssetDepositCommand = cli.Command{ + Name: "new", + ShortName: "n", + Usage: "Create a new TAP asset deposit.", + Description: "Create a new TAP asset deposit.", + Action: newAssetDeposit, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "asset_id", + Usage: "The asset id of the asset to deposit.", + }, + cli.Uint64Flag{ + Name: "amt", + Usage: "the amount to deposit (in asset " + + "units).", + }, + cli.UintFlag{ + Name: "expiry", + Usage: "the deposit expiry in blocks.", + }, + }, + } + + listAssetDepositsCommand = cli.Command{ + Name: "list", + ShortName: "l", + Usage: "List TAP asset deposits.", + Description: "List TAP asset deposits.", + Flags: []cli.Flag{ + cli.UintFlag{ + Name: "min_confs", + Usage: "The minimum amount of confirmations " + + "an anchor output should have to be " + + "listed.", + }, + cli.UintFlag{ + Name: "max_confs", + Usage: "The maximum number of confirmations " + + "an anchor output could have to be " + + "listed.", + }, + }, + Action: listAssetDeposits, + } + + withdrawAssetDepositCommand = cli.Command{ + Name: "withdraw", + ShortName: "w", + Usage: "Withdraw TAP asset deposits.", + Description: "Withdraw TAP asset deposits.", + Action: withdrawAssetDeposit, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "deposit_ids", + Usage: "The deposit ids of the asset " + + "deposits to withdraw.", + }, + }, + } + + testKeyRevealCommand = cli.Command{ + Name: "testkeyreveal", + ShortName: "tkr", + Usage: "Test revealing the key of a deposit to the server.", + Action: testKeyReveal, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "deposit_id", + Usage: "The deposit id of the asset deposit.", + }, + }, + } + + testCoSignCommand = cli.Command{ + Name: "testcosign", + ShortName: "tcs", + Usage: "Test co-signing a deposit to spend to an HTLC.", + Action: testCoSign, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "deposit_id", + Usage: "The deposit id of the asset deposit.", + }, + }, + } +) + +func init() { + commands = append(commands, assetDepositsCommands) +} + +func newAssetDeposit(ctx *cli.Context) error { + ctxb := context.Background() + if ctx.NArg() > 0 { + return cli.ShowCommandHelp(ctx, "newdeposit") + } + + client, cleanup, err := getAssetDepositsClient(ctx) + if err != nil { + return err + } + defer cleanup() + + assetID := ctx.String("asset_id") + amt := ctx.Uint64("amt") + expiry := int32(ctx.Uint("expiry")) + + resp, err := client.NewAssetDeposit( + ctxb, &looprpc.NewAssetDepositRequest{ + AssetId: assetID, + Amount: amt, + CsvExpiry: expiry, + }, + ) + if err != nil { + return err + } + + printJSON(resp) + + return nil +} + +func listAssetDeposits(ctx *cli.Context) error { + ctxb := context.Background() + if ctx.NArg() > 0 { + return cli.ShowCommandHelp(ctx, "list") + } + + client, cleanup, err := getAssetDepositsClient(ctx) + if err != nil { + return err + } + defer cleanup() + + resp, err := client.ListAssetDeposits( + ctxb, &looprpc.ListAssetDepositsRequest{ + MinConfs: uint32(ctx.Int("min_confs")), + MaxConfs: uint32(ctx.Int("max_confs")), + }) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func withdrawAssetDeposit(ctx *cli.Context) error { + ctxb := context.Background() + if ctx.NArg() > 0 { + return cli.ShowCommandHelp(ctx, "withdraw") + } + + client, cleanup, err := getAssetDepositsClient(ctx) + if err != nil { + return err + } + defer cleanup() + + depositIDs := ctx.StringSlice("deposit_ids") + + resp, err := client.WithdrawAssetDeposits( + ctxb, &looprpc.WithdrawAssetDepositsRequest{ + DepositIds: depositIDs, + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func testKeyReveal(ctx *cli.Context) error { + ctxb := context.Background() + if ctx.NArg() > 0 { + return cli.ShowCommandHelp(ctx, "testkeyreveal") + } + + client, cleanup, err := getAssetDepositsClient(ctx) + if err != nil { + return err + } + defer cleanup() + + depositID := ctx.String("deposit_id") + + resp, err := client.RevealAssetDepositKey( + ctxb, &looprpc.RevealAssetDepositKeyRequest{ + DepositId: depositID, + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} + +func testCoSign(ctx *cli.Context) error { + ctxb := context.Background() + if ctx.NArg() > 0 { + return cli.ShowCommandHelp(ctx, "testcosign") + } + + client, cleanup, err := getAssetDepositsClient(ctx) + if err != nil { + return err + } + defer cleanup() + + depositID := ctx.String("deposit_id") + + resp, err := client.TestCoSignAssetDepositHTLC( + ctxb, &looprpc.TestCoSignAssetDepositHTLCRequest{ + DepositId: depositID, + }, + ) + if err != nil { + return err + } + + printRespJSON(resp) + + return nil +} diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 4544dfac4..f068959bb 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -185,22 +185,44 @@ func main() { } } -func getClient(ctx *cli.Context) (looprpc.SwapClientClient, func(), error) { +func getConn(ctx *cli.Context) (*grpc.ClientConn, func(), error) { rpcServer := ctx.GlobalString("rpcserver") tlsCertPath, macaroonPath, err := extractPathArgs(ctx) if err != nil { return nil, nil, err } + conn, err := getClientConn(rpcServer, tlsCertPath, macaroonPath) if err != nil { return nil, nil, err } cleanup := func() { conn.Close() } + return conn, cleanup, nil +} + +func getClient(ctx *cli.Context) (looprpc.SwapClientClient, func(), error) { + conn, cleanup, err := getConn(ctx) + if err != nil { + return nil, nil, err + } + loopClient := looprpc.NewSwapClientClient(conn) return loopClient, cleanup, nil } +func getAssetDepositsClient(ctx *cli.Context) ( + looprpc.AssetDepositClientClient, func(), error) { + + conn, cleanup, err := getConn(ctx) + if err != nil { + return nil, nil, err + } + + assetDepositsClient := looprpc.NewAssetDepositClientClient(conn) + return assetDepositsClient, cleanup, nil +} + func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate) } diff --git a/loopd/daemon.go b/loopd/daemon.go index 7212933ad..de7e7306c 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -16,6 +16,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/assets" + asset_deposit "github.com/lightninglabs/loop/assets/deposit" "github.com/lightninglabs/loop/instantout" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightninglabs/loop/loopdb" @@ -248,6 +249,11 @@ func (d *Daemon) startWebServers() error { ) loop_looprpc.RegisterSwapClientServer(d.grpcServer, d) + // Register the asset deposit sub-server within the grpc server. + loop_looprpc.RegisterAssetDepositClientServer( + d.grpcServer, d.swapClientServer.assetDepositServer, + ) + // Register our debug server if it is compiled in. d.registerDebugServer() @@ -690,6 +696,10 @@ func (d *Daemon) initialize(withMacaroonService bool) error { ) } + // If the deposit manager is nil, the server will reutrn Unimplemented + // error for all RPCs. + assetDepositServer := asset_deposit.NewServer() + // Now finally fully initialize the swap client RPC server instance. d.swapClientServer = swapClientServer{ config: d.cfg, @@ -707,6 +717,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { depositManager: depositManager, withdrawalManager: withdrawalManager, staticLoopInManager: staticLoopInManager, + assetDepositServer: assetDepositServer, assetClient: d.assetClient, } diff --git a/loopd/log.go b/loopd/log.go index 0b29d4b32..830861db3 100644 --- a/loopd/log.go +++ b/loopd/log.go @@ -7,6 +7,7 @@ import ( "github.com/lightninglabs/aperture/l402" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/assets/deposit" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout" "github.com/lightninglabs/loop/instantout/reservation" @@ -92,6 +93,9 @@ func SetupLoggers(root *build.SubLoggerManager, intercept signal.Interceptor) { lnd.AddSubLogger( root, sweep.Subsystem, intercept, sweep.UseLogger, ) + lnd.AddSubLogger( + root, deposit.Subsystem, intercept, deposit.UseLogger, + ) } // genSubLogger creates a logger for a subsystem. We provide an instance of diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 313db6fc3..fc0cb2b82 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -22,6 +22,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/assets" + asset_deposit "github.com/lightninglabs/loop/assets/deposit" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout" "github.com/lightninglabs/loop/instantout/reservation" @@ -97,6 +98,7 @@ type swapClientServer struct { depositManager *deposit.Manager withdrawalManager *withdraw.Manager staticLoopInManager *loopin.Manager + assetDepositServer *asset_deposit.Server assetClient *assets.TapdClient swaps map[lntypes.Hash]loop.SwapInfo subscribers map[int]chan<- interface{} From 3a213d0c794b3c362e36e0b32629bfb6aab175b0 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:46:58 +0200 Subject: [PATCH 08/19] loopd: add profiler to the Loop daemon --- loopd/daemon.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/loopd/daemon.go b/loopd/daemon.go index de7e7306c..fca275b21 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + _ "net/http/pprof" "strings" "sync" "sync/atomic" @@ -133,6 +134,16 @@ func (d *Daemon) Start() error { return errOnlyStartOnce } + go func() { + http.Handle("/", http.RedirectHandler( + "/debug/pprof", http.StatusSeeOther, + )) + + listenAddr := fmt.Sprintf(":%d", 4321) + infof("Starting profile server at %s", listenAddr) + fmt.Println(http.ListenAndServe(listenAddr, nil)) // nolint: gosec + }() + network := lndclient.Network(d.cfg.Network) var err error From 606efa39f3bed92979281490711752a82d3017ea Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:56:19 +0200 Subject: [PATCH 09/19] assets: add asset deposit sweeper This commit adds a sweeper tailored for asset deposits, capable of sweeping a TAP deposit (a Kit) using either the timeout path or a revealed co-signer key. This lays the groundwork for implementing client-specific sweeping logic. --- assets/deposit/sweeper.go | 335 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 assets/deposit/sweeper.go diff --git a/assets/deposit/sweeper.go b/assets/deposit/sweeper.go new file mode 100644 index 000000000..e6d7192fb --- /dev/null +++ b/assets/deposit/sweeper.go @@ -0,0 +1,335 @@ +package deposit + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/assets" + "github.com/lightninglabs/loop/utils" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +// Sweeper is a higher level type that provides methods to sweep asset deposits. +type Sweeper struct { + tapdClient *assets.TapdClient + walletKit lndclient.WalletKitClient + signer lndclient.SignerClient + + addressParams address.ChainParams +} + +// NewSweeper creates a new Sweeper instance. +func NewSweeper(tapdClient *assets.TapdClient, + walletKit lndclient.WalletKitClient, signer lndclient.SignerClient, + addressParams address.ChainParams) *Sweeper { + + return &Sweeper{ + tapdClient: tapdClient, + walletKit: walletKit, + signer: signer, + addressParams: addressParams, + } +} + +// PublishDepositSweepMuSig2 publishes a deposit sweep using the MuSig2 keyspend +// path. +func (s *Sweeper) PublishDepositSweepMuSig2(ctx context.Context, deposit *Kit, + funder bool, depositProof *proof.Proof, + otherInternalKey *btcec.PrivateKey, sweepAddr *address.Tap, + feeRate chainfee.SatPerVByte, lockID wtxmgr.LockID, + lockDuration time.Duration) (*taprpc.SendAssetResponse, error) { + + // Verify that the proof is valid for the deposit and get the root hash + // which we will be using as our taproot tweak. + rootHash, err := deposit.VerifyProof(depositProof) + if err != nil { + log.Errorf("failed to verify deposit proof: %v", err) + + return nil, err + } + + // Now we can create the sweep vpacket which is simply sweeping the + // asset on the OP_TRUE output to the timeout sweep address. + sweepVpkt, err := assets.CreateOpTrueSweepVpkt( + ctx, []*proof.Proof{depositProof}, sweepAddr, &s.addressParams, + ) + if err != nil { + return nil, err + } + + // Gather the list of leased UTXOs that are used for the deposit sweep. + // This is needed to ensure that the UTXOs are correctly reused if we + // re-publish the deposit sweep. + leases, err := s.walletKit.ListLeases(ctx) + if err != nil { + return nil, err + } + + var leasedUtxos []lndclient.LeaseDescriptor + for _, lease := range leases { + if lease.LockID == lockID { + leasedUtxos = append(leasedUtxos, lease) + } + } + + // By committing the virtual transaction to the BTC template we created, + // the underlying lnd node will fund the BTC level transaction with an + // input to pay for the fees (and it will also add a change output). + sweepBtcPkt, activeAssets, passiveAssets, commitResp, err := + s.tapdClient.PrepareAndCommitVirtualPsbts( + ctx, sweepVpkt, feeRate, nil, s.addressParams.Params, + leasedUtxos, &lockID, lockDuration, + ) + if err != nil { + return nil, err + } + + prevOutFetcher := wallet.PsbtPrevOutputFetcher(sweepBtcPkt) + sigHash, err := getSigHash(sweepBtcPkt.UnsignedTx, 0, prevOutFetcher) + if err != nil { + return nil, err + } + + tweaks := &input.MuSig2Tweaks{ + TaprootTweak: rootHash[:], + } + + pubKey := deposit.FunderScriptKey + otherInternalPubKey := deposit.CoSignerInternalKey + if !funder { + pubKey = deposit.CoSignerScriptKey + otherInternalPubKey = deposit.FunderInternalKey + } + + internalPubKey, internalKey, err := DeriveSharedDepositKey( + ctx, s.signer, pubKey, + ) + + finalSig, err := utils.MuSig2Sign( + input.MuSig2Version100RC2, + []*btcec.PrivateKey{internalKey, otherInternalKey}, + []*btcec.PublicKey{internalPubKey, otherInternalPubKey}, + tweaks, sigHash, + ) + if err != nil { + return nil, err + } + + // Make sure that the signature is valid for the tx sighash and deposit + // internal key. + schnorrSig, err := schnorr.ParseSignature(finalSig) + if err != nil { + return nil, err + } + + // Calculate the final, tweaked MuSig2 output key. + taprootOutputKey := txscript.ComputeTaprootOutputKey( + deposit.MuSig2Key.PreTweakedKey, rootHash[:], + ) + + // Make sure we always return the parity stripped key. + taprootOutputKey, _ = schnorr.ParsePubKey(schnorr.SerializePubKey( + taprootOutputKey, + )) + + // Finally, verify that the signature is valid for the sighash and + // tweaked MuSig2 output key. + if !schnorrSig.Verify(sigHash[:], taprootOutputKey) { + return nil, fmt.Errorf("invalid signature") + } + + // Create the witness and add it to the sweep packet. + var buf bytes.Buffer + err = psbt.WriteTxWitness(&buf, wire.TxWitness{finalSig}) + if err != nil { + return nil, err + } + + sweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes() + + // Sign and finalize the sweep packet. + signedBtcPacket, err := s.walletKit.SignPsbt(ctx, sweepBtcPkt) + if err != nil { + return nil, err + } + + finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt( + ctx, signedBtcPacket, "", + ) + if err != nil { + return nil, err + } + + // Finally publish the sweep and log the transfer. + skipBroadcast := false + sendAssetResp, err := s.tapdClient.LogAndPublish( + ctx, finalizedBtcPacket, activeAssets, passiveAssets, + commitResp, skipBroadcast, + ) + + return sendAssetResp, err +} + +// PublishDepositTimeoutSweep publishes a deposit timeout sweep using the +// timeout script spend path. +func (s *Sweeper) PublishDepositTimeoutSweep(ctx context.Context, deposit *Kit, + depositProof *proof.Proof, sweepAddr *address.Tap, + feeRate chainfee.SatPerVByte, lockID wtxmgr.LockID, + lockDuration time.Duration) (*taprpc.SendAssetResponse, error) { + + // Create the sweep vpacket which is simply sweeping the asset on the + // OP_TRUE output to the timeout sweep address. + sweepVpkt, err := assets.CreateOpTrueSweepVpkt( + ctx, []*proof.Proof{depositProof}, sweepAddr, + &s.addressParams, + ) + if err != nil { + log.Errorf("Unable to create timeout sweep vpkt: %v", err) + + return nil, err + } + + // Gather the list of leased UTXOs that are used for the deposit sweep. + // This is needed to ensure that the UTXOs are correctly reused if we + // re-publish the deposit sweep. + leases, err := s.walletKit.ListLeases(ctx) + if err != nil { + log.Errorf("Unable to list leases: %v", err) + + return nil, err + } + + var leasedUtxos []lndclient.LeaseDescriptor + for _, lease := range leases { + if lease.LockID == lockID { + leasedUtxos = append(leasedUtxos, lease) + } + } + + // By committing the virtual transaction to the BTC template we created, + // the underlying lnd node will fund the BTC level transaction with an + // input to pay for the fees (and it will also add a change output). + timeoutSweepBtcPkt, activeAssets, passiveAssets, commitResp, err := + s.tapdClient.PrepareAndCommitVirtualPsbts( + ctx, sweepVpkt, feeRate, nil, + s.addressParams.Params, leasedUtxos, + &lockID, lockDuration, + ) + if err != nil { + log.Errorf("Unable to prepare and commit virtual psbt: %v", + err) + } + + // Create the witness for the timeout sweep. + witness, err := deposit.CreateTimeoutWitness( + ctx, s.signer, depositProof, timeoutSweepBtcPkt, + ) + if err != nil { + log.Errorf("Unable to create timeout witness: %v", err) + + return nil, err + } + + // Now add the witness to the sweep packet. + var buf bytes.Buffer + err = psbt.WriteTxWitness(&buf, witness) + if err != nil { + log.Errorf("Unable to write witness to buffer: %v", err) + + return nil, err + } + + timeoutSweepBtcPkt.Inputs[0].SighashType = txscript.SigHashDefault + timeoutSweepBtcPkt.Inputs[0].FinalScriptWitness = buf.Bytes() + + // Sign and finalize the sweep packet. + signedBtcPacket, err := s.walletKit.SignPsbt(ctx, timeoutSweepBtcPkt) + if err != nil { + log.Errorf("Unable to sign timeout sweep packet: %v", err) + + return nil, err + } + + finalizedBtcPacket, _, err := s.walletKit.FinalizePsbt( + ctx, signedBtcPacket, "", + ) + if err != nil { + log.Errorf("Unable to finalize timeout sweep packet: %v", err) + + return nil, err + } + + anchorTxHash := depositProof.AnchorTx.TxHash() + depositOutIdx := depositProof.InclusionProof.OutputIndex + + // Register the deposit transfer. This essentially materializes an asset + // "out of thin air" to ensure that LogAndPublish succeeds and the asset + // balance will be updated correctly. + depositScriptKey := depositProof.Asset.ScriptKey.PubKey + _, err = s.tapdClient.RegisterTransfer( + ctx, &taprpc.RegisterTransferRequest{ + AssetId: deposit.AssetID[:], + GroupKey: nil, + ScriptKey: depositScriptKey.SerializeCompressed(), + Outpoint: &taprpc.OutPoint{ + Txid: anchorTxHash[:], + OutputIndex: depositOutIdx, + }, + }, + ) + if err != nil { + if !strings.Contains(err.Error(), "proof already exists") { + log.Errorf("Unable to register deposit transfer: %v", + err) + + return nil, err + } + } + + // Publish the timeout sweep and log the transfer. + sendAssetResp, err := s.tapdClient.LogAndPublish( + ctx, finalizedBtcPacket, activeAssets, passiveAssets, + commitResp, false, + ) + if err != nil { + log.Errorf("Failed to publish timeout sweep: %v", err) + + return nil, err + } + + return sendAssetResp, nil +} + +// getSigHash calculates the signature hash for the given transaction. +func getSigHash(tx *wire.MsgTx, idx int, + prevOutFetcher txscript.PrevOutputFetcher) ([32]byte, error) { + + var sigHash [32]byte + + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + taprootSigHash, err := txscript.CalcTaprootSignatureHash( + sigHashes, txscript.SigHashDefault, tx, idx, prevOutFetcher, + ) + if err != nil { + return sigHash, err + } + + copy(sigHash[:], taprootSigHash) + + return sigHash, nil +} From b10f049f8052c3d97c067bc95b840b42ae4b795d Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:12:12 +0200 Subject: [PATCH 10/19] loopdb: add basic schema for asset deposits --- .../migrations/000017_asset_deposits.down.sql | 1 + .../migrations/000017_asset_deposits.up.sql | 97 +++++++++++++++++++ loopdb/sqlc/models.go | 34 +++++++ 3 files changed, 132 insertions(+) create mode 100644 loopdb/sqlc/migrations/000017_asset_deposits.down.sql create mode 100644 loopdb/sqlc/migrations/000017_asset_deposits.up.sql diff --git a/loopdb/sqlc/migrations/000017_asset_deposits.down.sql b/loopdb/sqlc/migrations/000017_asset_deposits.down.sql new file mode 100644 index 000000000..3ac57554b --- /dev/null +++ b/loopdb/sqlc/migrations/000017_asset_deposits.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS asset_deposits; diff --git a/loopdb/sqlc/migrations/000017_asset_deposits.up.sql b/loopdb/sqlc/migrations/000017_asset_deposits.up.sql new file mode 100644 index 000000000..5603f6b79 --- /dev/null +++ b/loopdb/sqlc/migrations/000017_asset_deposits.up.sql @@ -0,0 +1,97 @@ +CREATE TABLE IF NOT EXISTS asset_deposits ( + deposit_id TEXT PRIMARY KEY, + + -- protocol_version is the protocol version that the deposit was + -- created with. + protocol_version INTEGER NOT NULL, + + -- created_at is the time at which the deposit was created. + created_at TIMESTAMP NOT NULL, + + -- asset_id is the asset that is being deposited. + asset_id BLOB NOT NULL, + + -- amount is the amount of the deposit in asset units. + amount BIGINT NOT NULL, + + -- client_script_pubkey is the key used for the deposit script path as well + -- as the ephemeral key used for deriving the client's internal key. + client_script_pubkey BLOB NOT NULL, + + -- server_script_pubkey is the server's key that is used to construct the + -- deposit spending HTLC. + server_script_pubkey BLOB NOT NULL, + + -- client_internal_pubkey is the key derived from the shared secret + -- which is derived from the client's script key. + client_internal_pubkey BLOB NOT NULL, + + -- server_internal_pubkey is the server side public key that is used to + -- construct the 2-of-2 MuSig2 anchor output that holds the deposited + -- funds. + server_internal_pubkey BLOB NOT NULL, + + -- server_internal_key is the revealed private key corresponding to the + -- server's internal public key. It is only revealed when the deposit is + -- cooperatively withdrawn and therefore may be NULL. Note that the value + -- may be encrypted. + server_internal_key BYTEA, + + -- expiry denotes the CSV delay at which funds at a specific static address + -- can be swept back to the client. + expiry INT NOT NULL, + + -- client_key_family is the key family of the client's script public key + -- from the client's lnd wallet. + client_key_family INT NOT NULL, + + -- client_key_index is the key index of the client's script public key from + -- the client's lnd wallet. + client_key_index INT NOT NULL, + + -- addr is the TAP deposit address that the client should send the funds to. + addr TEXT NOT NULL UNIQUE, + + -- confirmation_height is the block height at which the deposit was + -- confirmed on-chain. + confirmation_height INT, + + -- outpoint is the outpoint of the confirmed deposit. + outpoint TEXT, + + -- pk_script is the pkscript of the deposit anchor output. + pk_script BLOB, + + -- sweep_addr is the address we'll use to sweep back the deposit to if it + -- has timed out or withdrawn cooperatively. + sweep_addr TEXT +); + +-- asset_deposit_updates contains all the updates to an asset deposit. +CREATE TABLE IF NOT EXISTS asset_deposit_updates ( + -- id is the auto incrementing primary key. + id INTEGER PRIMARY KEY, + + -- deposit_id is the unique identifier for the deposit. + deposit_id TEXT NOT NULL REFERENCES asset_deposits(deposit_id), + + -- update_state is the state of the deposit at the time of the update. + update_state INT NOT NULL, + + -- update_timestamp is the timestamp of the update. + update_timestamp TIMESTAMP NOT NULL +); + +-- asset_deposit_leased_utxos contains all the UTXOs that were leased to a +-- particular deposit. These leased UTXOs are used to fund the deposit timeout +-- sweep transaction. +CREATE TABLE IF NOT EXISTS asset_deposit_leased_utxos ( + -- id is the auto incrementing primary key. + id INTEGER PRIMARY KEY, + + -- deposit_id is the unique identifier for the deposit. + deposit_id TEXT NOT NULL REFERENCES asset_deposits(deposit_id), + + -- outpoint is the outpoint of the UTXO that was leased. + outpoint TEXT NOT NULL +); diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 6728bb420..fd7f18dd2 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -9,6 +9,40 @@ import ( "time" ) +type AssetDeposit struct { + DepositID string + ProtocolVersion int32 + CreatedAt time.Time + AssetID []byte + Amount int64 + ClientScriptPubkey []byte + ServerScriptPubkey []byte + ClientInternalPubkey []byte + ServerInternalPubkey []byte + ServerInternalKey []byte + Expiry int32 + ClientKeyFamily int32 + ClientKeyIndex int32 + Addr string + ConfirmationHeight sql.NullInt32 + Outpoint sql.NullString + PkScript []byte + SweepAddr sql.NullString +} + +type AssetDepositLeasedUtxo struct { + ID int32 + DepositID string + Outpoint string +} + +type AssetDepositUpdate struct { + ID int32 + DepositID string + UpdateState int32 + UpdateTimestamp time.Time +} + type Deposit struct { ID int32 DepositID []byte From 40ceaf05c67ed083925728e9f5b828bf35f2c522 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:42:25 +0200 Subject: [PATCH 11/19] assets+loopd: add scaffolding for the asset deposit manager This commit extends the deposit package with the `Deposit` type and a bare-bones `Manager`, along with the structural definition of the `SQLStore` which together implement the basic structure, run loop and storage for the deposit manager. It is not yet functional and serves as a foundation for future commits that will gradually extend its functionality. --- assets/deposit/deposit.go | 202 ++++++++++++++++++++++++++ assets/deposit/manager.go | 272 ++++++++++++++++++++++++++++++++++++ assets/deposit/protocol.go | 44 ++++++ assets/deposit/server.go | 8 +- assets/deposit/sql_store.go | 54 +++++++ loopd/daemon.go | 44 +++++- loopd/swapclient_server.go | 2 +- 7 files changed, 621 insertions(+), 5 deletions(-) create mode 100644 assets/deposit/deposit.go create mode 100644 assets/deposit/manager.go create mode 100644 assets/deposit/protocol.go create mode 100644 assets/deposit/sql_store.go diff --git a/assets/deposit/deposit.go b/assets/deposit/deposit.go new file mode 100644 index 000000000..f3fdb0657 --- /dev/null +++ b/assets/deposit/deposit.go @@ -0,0 +1,202 @@ +package deposit + +import ( + "encoding/hex" + "fmt" + "time" + + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/taproot-assets/proof" +) + +// State is the enum used for deposit states. +type State uint8 + +const ( + // StateInitiated indicates that the deposit has been initiated by the + // client. + StateInitiated State = 0 + + // StatePending indicates that the deposit is pending confirmation on + // the blockchain. + StatePending State = 1 + + // StateConfirmed indicates that the deposit has been confirmed on the + // blockchain. + StateConfirmed State = 2 + + // StateExpired indicates that the deposit has expired. + StateExpired State = 3 + + // StateTimeoutSweepPublished indicates that the timeout sweep has been + // published. + StateTimeoutSweepPublished State = 4 + + // StateWithdrawn indicates that the deposit has been withdrawn. + StateWithdrawn State = 5 + + // StateCooperativeSweepPublished indicates that the cooperative sweep + // withdrawing the deposit has been published. + StateCooperativeSweepPublished State = 6 + + // StateKeyRevealed indicates that the client has revealed a valid key + // for the deposit which is now ready to be swept. + StateKeyRevealed State = 7 + + // StateSpent indicates that the deposit has been spent. + StateSpent State = 8 + + // StateSwept indicates that the deposit has been swept, either by a + // timeout sweep or a cooperative (ie withdrawal) sweep. + StateSwept State = 9 +) + +// String coverts a deposit state to human readable string. +func (s State) String() string { + switch s { + case StateInitiated: + return "Initiated" + + case StatePending: + return "Pending" + + case StateConfirmed: + return "Confirmed" + + case StateExpired: + return "Expired" + + case StateTimeoutSweepPublished: + return "TimeoutSweepPublished" + + case StateWithdrawn: + return "Withdrawn" + + case StateCooperativeSweepPublished: + return "CooperativeSweepPublished" + + case StateKeyRevealed: + return "KeyRevealed" + + case StateSpent: + return "Spent" + + case StateSwept: + return "Swept" + + default: + return "Unknown" + } +} + +// IsFinal returns true if the deposit state is final, meaning that no further +// actions can be taken on the deposit. +func (s State) IsFinal() bool { + return s == StateSpent || s == StateSwept +} + +// DepositInfo holds publicly available information about an asset deposit. +// It is used to communicate deposit details to clients of the deposit Manager. +type DepositInfo struct { + // ID is the unique identifier for this deposit which will also be used + // to store the deposit in both the server and client databases. + ID string + + // Verison is the protocol version of the deposit. + Version AssetDepositProtocolVersion + + // CreatedAt is the time when the deposit was created (on the client). + CreatedAt time.Time + + // Amount is the amount of asset to be deposited. + Amount uint64 + + // Addr is the TAP deposit address where the asset will be sent. + Addr string + + // State is the deposit state. + State State + + // ConfirmationHeight is the block height at which the deposit was + // confirmed. + ConfirmationHeight uint32 + + // Outpoint is the anchor outpoint of the deposit. It is only set if the + // deposit has been confirmed. + Outpoint *wire.OutPoint + + // Expiry is the block height at which the deposit will expire. It is + // only set if the deposit has been confirmed. + Expiry uint32 + + // SweepAddr is the address we'll use to sweep the deposit back after + // timeout or if cooperatively withdrawing. + SweepAddr string +} + +// Copy creates a copy of the DepositInfo struct. +func (d *DepositInfo) Copy() *DepositInfo { + info := &DepositInfo{ + ID: d.ID, + Version: d.Version, + CreatedAt: d.CreatedAt, + Amount: d.Amount, + Addr: d.Addr, + State: d.State, + ConfirmationHeight: d.ConfirmationHeight, + Expiry: d.Expiry, + SweepAddr: d.SweepAddr, + } + + if d.Outpoint != nil { + info.Outpoint = &wire.OutPoint{ + Hash: d.Outpoint.Hash, + Index: d.Outpoint.Index, + } + } + + return info +} + +// Deposit is the struct that holds all the information about an asset deposit. +type Deposit struct { + *Kit + + *DepositInfo + + // PkScript is the pkscript of the deposit anchor output. + PkScript []byte + + // Proof is the proof of the deposit transfer. + Proof *proof.Proof + + // AnchorRootHash is the root hash of the deposit anchor output. + AnchorRootHash []byte +} + +// label returns a string label that we can use for marking a transfer funding +// the deposit. This is useful if we need to filter deposits. +func (d *Deposit) label() string { + return fmt.Sprintf("deposit %v", d.ID) +} + +// lockID converts a deposit ID to a lock ID. The lock ID is used to lock inputs +// used for the deposit sweep transaction. Note that we assume that the deposit +// ID is a hex-encoded string of the same length as the lock ID. +func (d *Deposit) lockID() (wtxmgr.LockID, error) { + var lockID wtxmgr.LockID + depositIDBytes, err := hex.DecodeString(d.ID) + if err != nil { + return wtxmgr.LockID{}, err + } + + if len(depositIDBytes) != len(lockID) { + return wtxmgr.LockID{}, fmt.Errorf("invalid deposit ID "+ + "length: %d", len(depositIDBytes)) + } + + copy(lockID[:], depositIDBytes) + + return lockID, nil +} diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go new file mode 100644 index 000000000..519fc1ff5 --- /dev/null +++ b/assets/deposit/manager.go @@ -0,0 +1,272 @@ +package deposit + +import ( + "context" + "errors" + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/assets" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/lightninglabs/taproot-assets/address" +) + +var ( + // ErrManagerShuttingDown signals that the asset deposit manager is + // shutting down and that no further calls should be made to it. + ErrManagerShuttingDown = errors.New("asset deposit manager is " + + "shutting down") +) + +// DepositUpdateCallback is a callback that is called when a deposit state is +// updated. The callback receives the updated deposit info. +type DepositUpdateCallback func(*DepositInfo) + +// Manager is responsible for creating, funding, sweeping and spending asset +// deposits used in asset swaps. It implements low level deposit management. +type Manager struct { + // depositServiceClient is a deposit service client. + depositServiceClient swapserverrpc.AssetDepositServiceClient + + // walletKit is the backing lnd wallet to use for deposit operations. + walletKit lndclient.WalletKitClient + + // signer is the signer client of the backing lnd wallet. + signer lndclient.SignerClient + + // chainNotifier is the chain notifier client of the underlyng lnd node. + chainNotifier lndclient.ChainNotifierClient + + // tapClient is the tapd client handling the deposit assets. + tapClient *assets.TapdClient + + // addressParams holds the TAP specific network params. + addressParams address.ChainParams + + // store is the deposit SQL store. + store *SQLStore + + // sweeper is responsible for assembling and publishing deposit sweeps. + sweeper *Sweeper + + // currentHeight is the current block height of the chain. + currentHeight uint32 + + // deposits is a map of all active deposits. The key is the deposit ID. + deposits map[string]*Deposit + + // subscribers is a map of all registered deposit update subscribers. + // The key is the deposit ID. + subscribers map[string][]DepositUpdateCallback + + // callEnter is used to sequentialize calls to the batch handler's + // main event loop. + callEnter chan struct{} + + // callLeave is used to resume the execution flow of the batch handler's + // main event loop. + callLeave chan struct{} + + // criticalErrChan is used to signal that a critical error has occurred + // and that the manager should stop. + criticalErrChan chan error + + // quit is owned by the parent batcher and signals that the batch must + // stop. + quit chan struct{} + + // runCtx is a function that returns the Manager's run-loop context. + runCtx func() context.Context +} + +// NewManager constructs a new asset deposit manager. +func NewManager(depositServiceClient swapserverrpc.AssetDepositServiceClient, + walletKit lndclient.WalletKitClient, signer lndclient.SignerClient, + chainNotifier lndclient.ChainNotifierClient, + tapClient *assets.TapdClient, store *SQLStore, + params *chaincfg.Params) *Manager { + + addressParams := address.ParamsForChain(params.Name) + sweeper := NewSweeper(tapClient, walletKit, signer, addressParams) + + return &Manager{ + depositServiceClient: depositServiceClient, + walletKit: walletKit, + signer: signer, + chainNotifier: chainNotifier, + tapClient: tapClient, + store: store, + sweeper: sweeper, + addressParams: addressParams, + deposits: make(map[string]*Deposit), + subscribers: make(map[string][]DepositUpdateCallback), + callEnter: make(chan struct{}), + callLeave: make(chan struct{}), + criticalErrChan: make(chan error, 1), + quit: make(chan struct{}), + } +} + +// Run is the entry point running that starts up the deposit manager and also +// runs the main event loop. +func (m *Manager) Run(ctx context.Context, bestBlock uint32) error { + log.Infof("Starting asset deposit manager") + defer log.Infof("Asset deposit manager stopped") + + ctxc, cancel := context.WithCancel(ctx) + defer func() { + // Signal to the main event loop that it should stop. + close(m.quit) + cancel() + }() + + // Set the context getter. + m.runCtx = func() context.Context { + return ctxc + } + + m.currentHeight = bestBlock + + blockChan, blockErrChan, err := m.chainNotifier.RegisterBlockEpochNtfn( + ctxc, + ) + if err != nil { + log.Errorf("unable to register for block epoch notifications: "+ + "%v", err) + + return err + } + + for { + select { + case <-m.callEnter: + <-m.callLeave + + case blockHeight, ok := <-blockChan: + if !ok { + return nil + } + + log.Debugf("Received block epoch notification: %v", + blockHeight) + + m.currentHeight = uint32(blockHeight) + err := m.handleBlockEpoch(ctxc, m.currentHeight) + if err != nil { + return err + } + + case err := <-blockErrChan: + log.Errorf("received error from block epoch "+ + "notification: %v", err) + + return err + + case err := <-m.criticalErrChan: + log.Errorf("stopping asset deposit manager due to "+ + "critical error: %v", err) + + return err + + case <-ctx.Done(): + return nil + + } + } +} + +// scheduleNextCall schedules the next call to the manager's main event loop. +// It returns a function that must be called when the call is finished. +func (m *Manager) scheduleNextCall() (func(), error) { + select { + case m.callEnter <- struct{}{}: + + case <-m.quit: + return func() {}, ErrManagerShuttingDown + } + + return func() { + m.callLeave <- struct{}{} + }, nil +} + +// criticalError is used to signal that a critical error has occurred. Such +// error will cause the manager to stop and return the (first) error to the +// caller of Run(...). +func (m *Manager) criticalError(err error) { + select { + case m.criticalErrChan <- err: + default: + } +} + +// handleBlockEpoch is called when a new block is added to the chain. +func (m *Manager) handleBlockEpoch(ctx context.Context, height uint32) error { + return nil +} + +// GetBestBlock returns the current best block height of the chain. +func (m *Manager) GetBestBlock() (uint32, error) { + done, err := m.scheduleNextCall() + if err != nil { + return 0, err + } + defer done() + + return m.currentHeight, nil +} + +// SubscribeDepositUpdates registers a subscriber for deposit state updates. +func (m *Manager) SubscribeDepositUpdates(depositID string, + subscriber DepositUpdateCallback) error { + + done, err := m.scheduleNextCall() + if err != nil { + return err + } + defer done() + + d, ok := m.deposits[depositID] + if !ok { + return fmt.Errorf("deposit %s not found", depositID) + } + + log.Infof("Registering deposit state update subscriber: %s", d.ID) + + // Note that for simplicity of design we do not check whether a + // subscriber is already registered for the deposit. + m.subscribers[d.ID] = append(m.subscribers[d.ID], subscriber) + + // Send the current deposit info to the subscriber right away. + subscriber(d.DepositInfo.Copy()) + + return nil +} + +// handleDepositStateUpdate updates the deposit state in the store and notifies +// all subscribers of the deposit state change. +func (m *Manager) handleDepositStateUpdate(ctx context.Context, + d *Deposit) error { + + log.Infof("Handling deposit state update: %s, state=%v", d.ID, d.State) + + // Store the deposit state update in the database. + err := m.store.UpdateDeposit(ctx, d) + if err != nil { + return err + } + + // Notify all subscribers of the deposit state update. + subscribers, ok := m.subscribers[d.ID] + if !ok { + log.Debugf("No subscribers for deposit %s", d.ID) + return nil + } + + for _, subscriber := range subscribers { + go subscriber(d.DepositInfo.Copy()) + } + + return nil +} diff --git a/assets/deposit/protocol.go b/assets/deposit/protocol.go new file mode 100644 index 000000000..3223e8cea --- /dev/null +++ b/assets/deposit/protocol.go @@ -0,0 +1,44 @@ +package deposit + +// AssetDepositProtocolVersion represents the protocol version for asset +// deposits. +type AssetDepositProtocolVersion uint32 + +const ( + // ProtocolVersion_V0 indicates that the client is a legacy version + // that did not report its protocol version. + ProtocolVersion_V0 AssetDepositProtocolVersion = 0 + + // stableProtocolVersion defines the current stable RPC protocol + // version. + stableProtocolVersion = ProtocolVersion_V0 +) + +var ( + // currentRPCProtocolVersion holds the version of the RPC protocol + // that the client selected to use for new swaps. Shouldn't be lower + // than the previous protocol version. + currentRPCProtocolVersion = stableProtocolVersion +) + +// CurrentRPCProtocolVersion returns the RPC protocol version selected to be +// used for new swaps. +func CurrentProtocolVersion() AssetDepositProtocolVersion { + return currentRPCProtocolVersion +} + +// Valid returns true if the value of the AddressProtocolVersion is valid. +func (p AssetDepositProtocolVersion) Valid() bool { + return p <= AssetDepositProtocolVersion(stableProtocolVersion) +} + +// String returns the string representation of a protocol version. +func (p AssetDepositProtocolVersion) String() string { + switch p { + case ProtocolVersion_V0: + return "ASSET_DEPOSIT_V0" + + default: + return "Unknown" + } +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 8e3f3b43d..511a03b12 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -11,10 +11,14 @@ import ( // Server is the grpc server that serves the reservation service. type Server struct { looprpc.UnimplementedAssetDepositClientServer + + manager *Manager } -func NewServer() *Server { - return &Server{} +func NewServer(manager *Manager) *Server { + return &Server{ + manager: manager, + } } // NewAssetDeposit is the rpc endpoint for loop clients to request a new asset diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go new file mode 100644 index 000000000..d4335275c --- /dev/null +++ b/assets/deposit/sql_store.go @@ -0,0 +1,54 @@ +package deposit + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightningnetwork/lnd/clock" +) + +// Querier is a subset of the methods we need from the postgres.querier +// interface for the deposit store. +type Querier interface { + // This is intentionally left empty. +} + +// DepositBaseDB is the interface that contains all the queries generated +// by sqlc for the deposit store. It also includes the ExecTx method for +// executing a function in the context of a database transaction. +type DepositBaseDB interface { + Querier + + // ExecTx allows for executing a function in the context of a database + // transaction. + ExecTx(ctx context.Context, txOptions loopdb.TxOptions, + txBody func(Querier) error) error +} + +// SQLStore is the high level SQL store for deposits. +type SQLStore struct { + db DepositBaseDB + + clock clock.Clock + addressParams address.ChainParams +} + +// NewSQLStore creates a new SQLStore. +func NewSQLStore(db DepositBaseDB, clock clock.Clock, + params *chaincfg.Params) *SQLStore { + + return &SQLStore{ + db: db, + clock: clock, + addressParams: address.ParamsForChain(params.Name), + } +} + +// UpdateDeposit updates the deposit state and extends the depsoit update log +// the SQL store. +func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { + return nil +} diff --git a/loopd/daemon.go b/loopd/daemon.go index fca275b21..13cd8b1a6 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -434,6 +434,12 @@ func (d *Daemon) initialize(withMacaroonService bool) error { infof("Successfully migrated boltdb") } + // Lnd's GetInfo call supplies us with the current block height. + info, err := d.lnd.Client.GetInfo(d.mainCtx) + if err != nil { + return err + } + // Now that we know where the database will live, we'll go ahead and // open up the default implementation of it. chainParams, err := lndclient.Network(d.cfg.Network).ChainParams() @@ -511,6 +517,10 @@ func (d *Daemon) initialize(withMacaroonService bool) error { swapClient.Conn, ) + assetDepositClient := loop_swaprpc.NewAssetDepositServiceClient( + swapClient.Conn, + ) + // Both the client RPC server and the swap server client should stop // on main context cancel. So we create it early and pass it down. d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background()) @@ -592,6 +602,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { depositManager *deposit.Manager withdrawalManager *withdraw.Manager staticLoopInManager *loopin.Manager + assetDepositManager *asset_deposit.Manager ) // Static address manager setup. @@ -705,11 +716,25 @@ func (d *Daemon) initialize(withMacaroonService bool) error { instantOutManager = instantout.NewInstantOutManager( instantOutConfig, int32(blockHeight), ) + + if d.assetClient != nil { + depositStore := asset_deposit.NewSQLStore( + loopdb.NewTypedStore[asset_deposit.Querier]( + baseDb, + ), clock.NewDefaultClock(), d.lnd.ChainParams, + ) + assetDepositManager = asset_deposit.NewManager( + assetDepositClient, d.lnd.WalletKit, + d.lnd.Signer, d.lnd.ChainNotifier, + d.assetClient, depositStore, d.lnd.ChainParams, + ) + } + } // If the deposit manager is nil, the server will reutrn Unimplemented // error for all RPCs. - assetDepositServer := asset_deposit.NewServer() + assetDepositServer := asset_deposit.NewServer(assetDepositManager) // Now finally fully initialize the swap client RPC server instance. d.swapClientServer = swapClientServer{ @@ -728,8 +753,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error { depositManager: depositManager, withdrawalManager: withdrawalManager, staticLoopInManager: staticLoopInManager, - assetDepositServer: assetDepositServer, assetClient: d.assetClient, + assetDepositServer: assetDepositServer, } // Retrieve all currently existing swaps from the database. @@ -996,6 +1021,21 @@ func (d *Daemon) initialize(withMacaroonService bool) error { } } + if assetDepositManager != nil { + d.wg.Add(1) + + go func() { + defer d.wg.Done() + + err = assetDepositManager.Run( + d.mainCtx, info.BlockHeight, + ) + if err != nil && !errors.Is(context.Canceled, err) { + d.internalErrChan <- err + } + }() + } + // Last, start our internal error handler. This will return exactly one // error or nil on the main error channel to inform the caller that // something went wrong or that shutdown is complete. We don't add to diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index fc0cb2b82..61bfdf348 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -98,8 +98,8 @@ type swapClientServer struct { depositManager *deposit.Manager withdrawalManager *withdraw.Manager staticLoopInManager *loopin.Manager - assetDepositServer *asset_deposit.Server assetClient *assets.TapdClient + assetDepositServer *asset_deposit.Server swaps map[lntypes.Hash]loop.SwapInfo subscribers map[int]chan<- interface{} statusChan chan loop.SwapInfo From 607c6e0e81c28c25e5ad9bb490fb435b2e1c922a Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:59:41 +0200 Subject: [PATCH 12/19] assets+loopdb: implement new asset deposit functionality This commit extends the asset deposit manager along with the underlying sql store, adding functionality to create and fund new asset deposits. --- assets/deposit/manager.go | 328 +++++++++++++++++++++++++ assets/deposit/server.go | 49 +++- assets/deposit/sql_store.go | 84 ++++++- loopdb/sqlc/asset_deposits.sql.go | 110 +++++++++ loopdb/sqlc/querier.go | 3 + loopdb/sqlc/queries/asset_deposits.sql | 29 +++ 6 files changed, 599 insertions(+), 4 deletions(-) create mode 100644 loopdb/sqlc/asset_deposits.sql.go create mode 100644 loopdb/sqlc/queries/asset_deposits.sql diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 519fc1ff5..b5c8ef007 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -4,15 +4,24 @@ import ( "context" "errors" "fmt" + "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/assets" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/taprpc" ) var ( + // AssetDepositKeyFamily is the key family used for generating asset + // deposit keys. + AssetDepositKeyFamily = int32(1122) + // ErrManagerShuttingDown signals that the asset deposit manager is // shutting down and that no further calls should be made to it. ErrManagerShuttingDown = errors.New("asset deposit manager is " + @@ -270,3 +279,322 @@ func (m *Manager) handleDepositStateUpdate(ctx context.Context, return nil } + +// NewDeposit creates a new asset deposit with the given parameters. +func (m *Manager) NewDeposit(ctx context.Context, assetID asset.ID, + amount uint64, csvExpiry uint32) (DepositInfo, error) { + + clientKeyDesc, err := m.walletKit.DeriveNextKey( + ctx, AssetDepositKeyFamily, + ) + if err != nil { + return DepositInfo{}, err + } + clientInternalPubKey, _, err := DeriveSharedDepositKey( + ctx, m.signer, clientKeyDesc.PubKey, + ) + if err != nil { + return DepositInfo{}, err + } + + clientScriptPubKeyBytes := clientKeyDesc.PubKey.SerializeCompressed() + clientInternalPubKeyBytes := clientInternalPubKey.SerializeCompressed() + + resp, err := m.depositServiceClient.NewAssetDeposit( + ctx, &swapserverrpc.NewAssetDepositServerReq{ + AssetId: assetID[:], + Amount: amount, + ClientInternalPubkey: clientInternalPubKeyBytes, + ClientScriptPubkey: clientScriptPubKeyBytes, + CsvExpiry: int32(csvExpiry), + }, + ) + if err != nil { + log.Errorf("Swap server was unable to create the deposit: %v", + err) + + return DepositInfo{}, err + } + + serverScriptPubKey, err := btcec.ParsePubKey(resp.ServerScriptPubkey) + if err != nil { + return DepositInfo{}, err + } + + serverInternalPubKey, err := btcec.ParsePubKey( + resp.ServerInternalPubkey, + ) + if err != nil { + return DepositInfo{}, err + } + + kit, err := NewKit( + clientKeyDesc.PubKey, clientInternalPubKey, serverScriptPubKey, + serverInternalPubKey, clientKeyDesc.KeyLocator, assetID, + csvExpiry, &m.addressParams, + ) + if err != nil { + return DepositInfo{}, err + } + + deposit := &Deposit{ + Kit: kit, + DepositInfo: &DepositInfo{ + ID: resp.DepositId, + Version: CurrentProtocolVersion(), + CreatedAt: time.Now(), + Amount: amount, + Addr: resp.DepositAddr, + State: StateInitiated, + }, + } + + err = m.store.AddAssetDeposit(ctx, deposit) + if err != nil { + log.Errorf("Unable to add deposit to store: %v", err) + + return DepositInfo{}, err + } + + err = m.handleNewDeposit(ctx, deposit) + if err != nil { + log.Errorf("Unable to add deposit to active deposits: %v", err) + + return DepositInfo{}, err + } + + return *deposit.DepositInfo.Copy(), nil +} + +// handleNewDeposit adds the deposit to the active deposits map and starts the +// funding process, all on the main event loop goroutine. +func (m *Manager) handleNewDeposit(ctx context.Context, deposit *Deposit) error { + done, err := m.scheduleNextCall() + if err != nil { + return err + } + defer done() + + m.deposits[deposit.ID] = deposit + + return m.fundDepositIfNeeded(ctx, deposit) +} + +// fundDepositIfNeeded attempts to fund the passed deposit if it is not already +// funded. +func (m *Manager) fundDepositIfNeeded(ctx context.Context, d *Deposit) error { + // Now list transfers from tapd and check if the deposit is funded. + funded, transfer, outIndex, err := m.isDepositFunded(ctx, d) + if err != nil { + log.Errorf("Unable to check if deposit %v is funded: %v", d.ID, + err) + + return err + } + + if !funded { + // No funding transfer found, so we'll attempt to fund the + // deposit by sending the asset to the deposit address. Note + // that we label the send request with a specific label in order + // to be able to subscribe to send events with a label filter. + sendResp, err := m.tapClient.SendAsset( + ctx, &taprpc.SendAssetRequest{ + TapAddrs: []string{d.Addr}, + Label: d.label(), + }, + ) + if err != nil { + log.Errorf("Unable to send asset to deposit %v: %v", + d.ID, err) + + return err + } + + // Extract the funding outpoint from the transfer. + transfer, outIndex, err = d.GetMatchingOut( + d.Amount, []*taprpc.AssetTransfer{sendResp.Transfer}, + ) + if err != nil { + log.Errorf("Unable to get funding out for %v: %v ", + d.ID, err) + + return err + } + } + + log.Infof("Deposit %v is funded in anchor %x:%d, "+ + "anchor tx block height: %v", d.ID, + transfer.AnchorTxHash, outIndex, transfer.AnchorTxBlockHeight) + + // If the deposit is confirmed, then we don't need to wait for the + // confirmation to happen. + // TODO(bhandras): once backlog events are supported we can remove this. + if transfer.AnchorTxBlockHeight != 0 { + return m.markDepositConfirmed(ctx, d, transfer) + } + + // Wait for deposit confirmation otherwise. + err = m.waitForDepositConfirmation(m.runCtx(), d) + if err != nil { + log.Errorf("Unable to wait for deposit confirmation: %v", err) + + return err + } + + return nil +} + +// isDepositFunded checks if the deposit is funded with the expected amount. It +// does so by checking if there is a deposit output with the expected keys and +// amount in the list of transfers of the funder. +func (m *Manager) isDepositFunded(ctx context.Context, d *Deposit) (bool, + *taprpc.AssetTransfer, int, error) { + + res, err := m.tapClient.ListTransfers( + ctx, &taprpc.ListTransfersRequest{}, + ) + if err != nil { + return false, nil, 0, err + } + + transfer, outIndex, err := d.GetMatchingOut(d.Amount, res.Transfers) + if err != nil { + return false, nil, 0, err + } + + if transfer == nil { + return false, nil, 0, nil + } + + return true, transfer, outIndex, nil +} + +// waitForDepositConfirmation waits for the deposit to be confirmed. +// +// NOTE: currently SubscribeSendEvents does not support streaming backlog +// events. To avoid missing the confirmation event, we also poll asset transfers +// upon restart. +func (m *Manager) waitForDepositConfirmation(ctx context.Context, + d *Deposit) error { + + log.Infof("Subscribing to send events for pending deposit %s, "+ + "addr=%v, created_at=%v", d.ID, d.Addr, d.CreatedAt) + + resChan, errChan, err := m.tapClient.WaitForSendComplete( + ctx, nil, d.label(), + ) + if err != nil { + log.Errorf("unable to subscribe to send events: %v", err) + return err + } + + go func() { + select { + case res := <-resChan: + done, err := m.scheduleNextCall() + if err != nil { + log.Errorf("Unable to schedule next call: %v", + err) + + m.criticalError(err) + } + defer done() + + err = m.markDepositConfirmed(ctx, d, res.Transfer) + if err != nil { + log.Errorf("Unable to mark deposit %v as "+ + "confirmed: %v", d.ID, err) + + m.criticalError(err) + } + + case err := <-errChan: + m.criticalError(err) + } + }() + + return nil +} + +// cacheProofInfo caches the proof information for the deposit in-memory. +func (m *Manager) cacheProofInfo(ctx context.Context, d *Deposit) error { + proofFile, err := d.ExportProof(ctx, m.tapClient, d.Outpoint) + if err != nil { + log.Errorf("Unable to export proof for deposit %v: %v", d.ID, + err) + + return err + } + + // Import the proof in order to be able to spend the deposit later on + // either into an HTLC or a timeout sweep. + // + // TODO(bhandras): do we need to check/handle if/when the proof is + // already imported? + depositProof, err := m.tapClient.ImportProofFile( + ctx, proofFile.RawProofFile, + ) + if err != nil { + return err + } + + d.Proof = depositProof + + // Verify that the proof is valid for the deposit and get the root hash + // which we may use later when signing the HTLC transaction. + anchorRootHash, err := d.VerifyProof(depositProof) + if err != nil { + log.Errorf("failed to verify deposity proof: %v", err) + + return err + } + + d.AnchorRootHash = anchorRootHash + + return nil +} + +// markDepositConfirmed marks the deposit as confirmed in the store and moves it +// to the active deposits map. It also updates the outpoint and the confirmation +// height of the deposit. +func (m *Manager) markDepositConfirmed(ctx context.Context, d *Deposit, + transfer *taprpc.AssetTransfer) error { + + // Extract the funding outpoint from the transfer. + _, outIdx, err := d.GetMatchingOut( + d.Amount, []*taprpc.AssetTransfer{transfer}, + ) + if err != nil { + return err + } + + outpoint, err := wire.NewOutPointFromString( + transfer.Outputs[outIdx].Anchor.Outpoint, + ) + if err != nil { + log.Errorf("Unable to parse deposit outpoint %v: %v", + transfer.Outputs[outIdx].Anchor.Outpoint, err) + + return err + } + + d.Outpoint = outpoint + d.PkScript = transfer.Outputs[outIdx].Anchor.PkScript + d.ConfirmationHeight = transfer.AnchorTxBlockHeight + d.State = StateConfirmed + + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + return err + } + + err = m.cacheProofInfo(ctx, d) + if err != nil { + return err + } + + log.Infof("Deposit %v is confirmed at block %v", d.ID, + d.ConfirmationHeight) + + return nil +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 511a03b12..3f669792d 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -2,12 +2,22 @@ package deposit import ( "context" + "encoding/hex" + "fmt" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/taproot-assets/asset" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +var ( + // ErrAssetDepositsUnavailable is returned when the asset deposit + // service is not available. + ErrAssetDepositsUnavailable = status.Error(codes.Unavailable, + "asset deposits are unavailable") +) + // Server is the grpc server that serves the reservation service. type Server struct { looprpc.UnimplementedAssetDepositClientServer @@ -27,7 +37,44 @@ func (s *Server) NewAssetDeposit(ctx context.Context, in *looprpc.NewAssetDepositRequest) (*looprpc.NewAssetDepositResponse, error) { - return nil, status.Error(codes.Unimplemented, "unimplemented") + if s.manager == nil { + return nil, ErrAssetDepositsUnavailable + } + + assetIDBytes, err := hex.DecodeString(in.AssetId) + if err != nil { + return nil, status.Error(codes.InvalidArgument, + fmt.Sprintf("invalid asset ID encoding: %v", err)) + } + + var assetID asset.ID + if len(assetIDBytes) != len(assetID) { + return nil, fmt.Errorf("invalid asset ID lenght: expected "+ + "%v bytes, got %d", len(assetID), len(assetIDBytes)) + } + + copy(assetID[:], assetIDBytes) + + if in.Amount == 0 { + return nil, status.Error(codes.InvalidArgument, + "amount must be greater than zero") + } + + if in.CsvExpiry <= 0 { + return nil, status.Error(codes.InvalidArgument, + "CSV expiry must be greater than zero") + } + + depositInfo, err := s.manager.NewDeposit( + ctx, assetID, in.Amount, uint32(in.CsvExpiry), + ) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &looprpc.NewAssetDepositResponse{ + DepositId: depositInfo.ID, + }, nil } // ListAssetDeposits is the rpc endpoint for loop clients to list their asset diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index d4335275c..108576d63 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -2,10 +2,11 @@ package deposit import ( "context" - "fmt" + "database/sql" "github.com/btcsuite/btcd/chaincfg" "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/loopdb/sqlc" "github.com/lightninglabs/taproot-assets/address" "github.com/lightningnetwork/lnd/clock" ) @@ -13,7 +14,13 @@ import ( // Querier is a subset of the methods we need from the postgres.querier // interface for the deposit store. type Querier interface { - // This is intentionally left empty. + AddAssetDeposit(context.Context, sqlc.AddAssetDepositParams) error + + UpdateDepositState(ctx context.Context, + arg sqlc.UpdateDepositStateParams) error + + MarkDepositConfirmed(ctx context.Context, + arg sqlc.MarkDepositConfirmedParams) error } // DepositBaseDB is the interface that contains all the queries generated @@ -47,8 +54,79 @@ func NewSQLStore(db DepositBaseDB, clock clock.Clock, } } +// AddAssetDeposit adds a new asset deposit to the database. +func (s *SQLStore) AddAssetDeposit(ctx context.Context, d *Deposit) error { + txOptions := loopdb.NewSqlWriteOpts() + + createdAt := d.CreatedAt.UTC() + clientScriptPubKey := d.FunderScriptKey.SerializeCompressed() + clientInternalPubKey := d.FunderInternalKey.SerializeCompressed() + serverScriptPubKey := d.CoSignerScriptKey.SerializeCompressed() + serverInternalPubKey := d.CoSignerInternalKey.SerializeCompressed() + + return s.db.ExecTx(ctx, txOptions, func(tx Querier) error { + err := tx.AddAssetDeposit(ctx, sqlc.AddAssetDepositParams{ + DepositID: d.ID, + CreatedAt: createdAt, + AssetID: d.AssetID[:], + Amount: int64(d.Amount), + ClientScriptPubkey: clientScriptPubKey, + ClientInternalPubkey: clientInternalPubKey, + ServerScriptPubkey: serverScriptPubKey, + ServerInternalPubkey: serverInternalPubKey, + ClientKeyFamily: int32(d.KeyLocator.Family), + ClientKeyIndex: int32(d.KeyLocator.Index), + Expiry: int32(d.CsvExpiry), + Addr: d.Addr, + ProtocolVersion: int32(d.Version), + }) + if err != nil { + return err + } + + return tx.UpdateDepositState(ctx, sqlc.UpdateDepositStateParams{ + DepositID: d.ID, + UpdateState: int32(StateInitiated), + UpdateTimestamp: createdAt, + }) + }) +} + // UpdateDeposit updates the deposit state and extends the depsoit update log // the SQL store. func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { - return nil + txOptions := loopdb.NewSqlWriteOpts() + + return s.db.ExecTx(ctx, txOptions, func(tx Querier) error { + switch d.State { + case StateConfirmed: + err := tx.MarkDepositConfirmed( + ctx, sqlc.MarkDepositConfirmedParams{ + DepositID: d.ID, + ConfirmationHeight: sql.NullInt32{ + Int32: int32( + d.ConfirmationHeight, + ), + Valid: true, + }, + Outpoint: sql.NullString{ + String: d.Outpoint.String(), + Valid: true, + }, + PkScript: d.PkScript, + }, + ) + if err != nil { + return err + } + } + + return tx.UpdateDepositState( + ctx, sqlc.UpdateDepositStateParams{ + DepositID: d.ID, + UpdateState: int32(d.State), + UpdateTimestamp: s.clock.Now().UTC(), + }, + ) + }) } diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go new file mode 100644 index 000000000..9de22a865 --- /dev/null +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -0,0 +1,110 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: asset_deposits.sql + +package sqlc + +import ( + "context" + "database/sql" + "time" +) + +const addAssetDeposit = `-- name: AddAssetDeposit :exec +INSERT INTO asset_deposits ( + deposit_id, + protocol_version, + created_at, + asset_id, + amount, + client_script_pubkey, + server_script_pubkey, + client_internal_pubkey, + server_internal_pubkey, + server_internal_key, + client_key_family, + client_key_index, + expiry, + addr +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) +` + +type AddAssetDepositParams struct { + DepositID string + ProtocolVersion int32 + CreatedAt time.Time + AssetID []byte + Amount int64 + ClientScriptPubkey []byte + ServerScriptPubkey []byte + ClientInternalPubkey []byte + ServerInternalPubkey []byte + ServerInternalKey []byte + ClientKeyFamily int32 + ClientKeyIndex int32 + Expiry int32 + Addr string +} + +func (q *Queries) AddAssetDeposit(ctx context.Context, arg AddAssetDepositParams) error { + _, err := q.db.ExecContext(ctx, addAssetDeposit, + arg.DepositID, + arg.ProtocolVersion, + arg.CreatedAt, + arg.AssetID, + arg.Amount, + arg.ClientScriptPubkey, + arg.ServerScriptPubkey, + arg.ClientInternalPubkey, + arg.ServerInternalPubkey, + arg.ServerInternalKey, + arg.ClientKeyFamily, + arg.ClientKeyIndex, + arg.Expiry, + arg.Addr, + ) + return err +} + +const markDepositConfirmed = `-- name: MarkDepositConfirmed :exec +UPDATE asset_deposits +SET confirmation_height = $2, outpoint = $3, pk_script = $4 +WHERE deposit_id = $1 +` + +type MarkDepositConfirmedParams struct { + DepositID string + ConfirmationHeight sql.NullInt32 + Outpoint sql.NullString + PkScript []byte +} + +func (q *Queries) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error { + _, err := q.db.ExecContext(ctx, markDepositConfirmed, + arg.DepositID, + arg.ConfirmationHeight, + arg.Outpoint, + arg.PkScript, + ) + return err +} + +const updateDepositState = `-- name: UpdateDepositState :exec +INSERT INTO asset_deposit_updates ( + deposit_id, + update_state, + update_timestamp +) VALUES ($1, $2, $3) +` + +type UpdateDepositStateParams struct { + DepositID string + UpdateState int32 + UpdateTimestamp time.Time +} + +func (q *Queries) UpdateDepositState(ctx context.Context, arg UpdateDepositStateParams) error { + _, err := q.db.ExecContext(ctx, updateDepositState, arg.DepositID, arg.UpdateState, arg.UpdateTimestamp) + return err +} diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 15b2e388f..0e72ba01b 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -10,6 +10,7 @@ import ( ) type Querier interface { + AddAssetDeposit(ctx context.Context, arg AddAssetDepositParams) error AllDeposits(ctx context.Context) ([]Deposit, error) AllStaticAddresses(ctx context.Context) ([]StaticAddress, error) CancelBatch(ctx context.Context, id int32) error @@ -61,9 +62,11 @@ type Querier interface { InsertSwap(ctx context.Context, arg InsertSwapParams) error InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error IsStored(ctx context.Context, swapHash []byte) (bool, error) + MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error + UpdateDepositState(ctx context.Context, arg UpdateDepositStateParams) error UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error UpdateLoopOutAssetOffchainPayments(ctx context.Context, arg UpdateLoopOutAssetOffchainPaymentsParams) error UpdateReservation(ctx context.Context, arg UpdateReservationParams) error diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql new file mode 100644 index 000000000..dd02d5055 --- /dev/null +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -0,0 +1,29 @@ +-- name: AddAssetDeposit :exec +INSERT INTO asset_deposits ( + deposit_id, + protocol_version, + created_at, + asset_id, + amount, + client_script_pubkey, + server_script_pubkey, + client_internal_pubkey, + server_internal_pubkey, + server_internal_key, + client_key_family, + client_key_index, + expiry, + addr +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14); + +-- name: UpdateDepositState :exec +INSERT INTO asset_deposit_updates ( + deposit_id, + update_state, + update_timestamp +) VALUES ($1, $2, $3); + +-- name: MarkDepositConfirmed :exec +UPDATE asset_deposits +SET confirmation_height = $2, outpoint = $3, pk_script = $4 +WHERE deposit_id = $1; From 16259a2c39a212fc81c3a605971ae1c430e85d3d Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:06:24 +0200 Subject: [PATCH 13/19] assets+loopdb: implement functionality to list asset deposits This commit adds the necessary changes to the asset deposit manager, the underlying sql store and the asset deposit subserver to be able to list asset deposits. --- assets/deposit/manager.go | 37 ++++++++ assets/deposit/server.go | 39 +++++++- assets/deposit/sql_store.go | 120 +++++++++++++++++++++++++ loopdb/sqlc/asset_deposits.sql.go | 80 +++++++++++++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 12 +++ 6 files changed, 288 insertions(+), 1 deletion(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index b5c8ef007..74dfcebf3 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -598,3 +598,40 @@ func (m *Manager) markDepositConfirmed(ctx context.Context, d *Deposit, return nil } + +// ListDeposits returns all deposits that are in the given range of +// confirmations. +func (m *Manager) ListDeposits(ctx context.Context, minConfs, maxConfs uint32) ( + []Deposit, error) { + + bestBlock, err := m.GetBestBlock() + if err != nil { + return nil, err + } + + deposits, err := m.store.GetAllDeposits(ctx) + if err != nil { + return nil, err + } + + // Only filter based on confirmations if the user has set a min or max + // confs. + filterConfs := minConfs != 0 || maxConfs != 0 + + // Prefilter deposits based on the min/max confs. + filteredDeposits := make([]Deposit, 0, len(deposits)) + for _, deposit := range deposits { + if filterConfs { + // Check that the deposit suits our min/max confs + // criteria. + confs := bestBlock - deposit.ConfirmationHeight + if confs < minConfs || confs > maxConfs { + continue + } + } + + filteredDeposits = append(filteredDeposits, deposit) + } + + return filteredDeposits, nil +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 3f669792d..3bc1aaaab 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -83,7 +83,44 @@ func (s *Server) ListAssetDeposits(ctx context.Context, in *looprpc.ListAssetDepositsRequest) ( *looprpc.ListAssetDepositsResponse, error) { - return nil, status.Error(codes.Unimplemented, "unimplemented") + if s.manager == nil { + return nil, ErrAssetDepositsUnavailable + } + + if in.MinConfs < in.MaxConfs { + return nil, status.Error(codes.InvalidArgument, + "max_confs must be greater than or equal to min_confs") + } + + deposits, err := s.manager.ListDeposits(ctx, in.MinConfs, in.MaxConfs) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + filteredDeposits := make([]*looprpc.AssetDeposit, 0, len(deposits)) + for _, d := range deposits { + rpcDeposit := &looprpc.AssetDeposit{ + DepositId: d.ID, + CreatedAt: d.CreatedAt.Unix(), + AssetId: d.AssetID.String(), + Amount: d.Amount, + DepositAddr: d.Addr, + State: d.State.String(), + ConfirmationHeight: d.ConfirmationHeight, + Expiry: d.ConfirmationHeight + d.CsvExpiry, + SweepAddr: d.SweepAddr, + } + + if d.Outpoint != nil { + rpcDeposit.AnchorOutpoint = d.Outpoint.String() + } + + filteredDeposits = append(filteredDeposits, rpcDeposit) + } + + return &looprpc.ListAssetDepositsResponse{ + FilteredDeposits: filteredDeposits, + }, nil } // RevealAssetDepositKey is the rpc endpoint for loop clients to reveal the diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index 108576d63..93d904e7c 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -3,12 +3,17 @@ package deposit import ( "context" "database/sql" + "fmt" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb/sqlc" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/keychain" ) // Querier is a subset of the methods we need from the postgres.querier @@ -21,6 +26,9 @@ type Querier interface { MarkDepositConfirmed(ctx context.Context, arg sqlc.MarkDepositConfirmedParams) error + + GetAssetDeposits(ctx context.Context) ([]sqlc.GetAssetDepositsRow, + error) } // DepositBaseDB is the interface that contains all the queries generated @@ -130,3 +138,115 @@ func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { ) }) } + +func (s *SQLStore) GetAllDeposits(ctx context.Context) ([]Deposit, error) { + sqlDeposits, err := s.db.GetAssetDeposits(ctx) + if err != nil { + return nil, err + } + + deposits := make([]Deposit, 0, len(sqlDeposits)) + for _, sqlDeposit := range sqlDeposits { + deposit, err := sqlcDepositToDeposit( + sqlDeposit, &s.addressParams, + ) + if err != nil { + return nil, err + } + + deposits = append(deposits, deposit) + } + + return deposits, nil +} + +func sqlcDepositToDeposit(sqlDeposit sqlc.GetAssetDepositsRow, + addressParams *address.ChainParams) (Deposit, error) { + + clientScriptPubKey, err := btcec.ParsePubKey( + sqlDeposit.ClientScriptPubkey, + ) + if err != nil { + return Deposit{}, err + } + + serverScriptPubKey, err := btcec.ParsePubKey( + sqlDeposit.ServerScriptPubkey, + ) + if err != nil { + return Deposit{}, err + } + + clientInteralPubKey, err := btcec.ParsePubKey( + sqlDeposit.ClientInternalPubkey, + ) + if err != nil { + return Deposit{}, err + } + + serverInternalPubKey, err := btcec.ParsePubKey( + sqlDeposit.ServerInternalPubkey, + ) + if err != nil { + return Deposit{}, err + } + + clientKeyLocator := keychain.KeyLocator{ + Family: keychain.KeyFamily( + sqlDeposit.ClientKeyFamily, + ), + Index: uint32(sqlDeposit.ClientKeyIndex), + } + + if len(sqlDeposit.AssetID) != len(asset.ID{}) { + return Deposit{}, fmt.Errorf("malformed asset ID for deposit: "+ + "%v", sqlDeposit.DepositID) + } + + depositInfo := &DepositInfo{ + ID: sqlDeposit.DepositID, + Version: AssetDepositProtocolVersion( + sqlDeposit.ProtocolVersion, + ), + CreatedAt: sqlDeposit.CreatedAt.Local(), + Amount: uint64(sqlDeposit.Amount), + Addr: sqlDeposit.Addr, + State: State(sqlDeposit.UpdateState), + } + + if sqlDeposit.ConfirmationHeight.Valid { + depositInfo.ConfirmationHeight = uint32( + sqlDeposit.ConfirmationHeight.Int32, + ) + } + + if sqlDeposit.Outpoint.Valid { + outpoint, err := wire.NewOutPointFromString( + sqlDeposit.Outpoint.String, + ) + if err != nil { + return Deposit{}, err + } + + depositInfo.Outpoint = outpoint + } + + if sqlDeposit.SweepAddr.Valid { + depositInfo.SweepAddr = sqlDeposit.SweepAddr.String + } + + kit, err := NewKit( + clientScriptPubKey, clientInteralPubKey, serverScriptPubKey, + serverInternalPubKey, clientKeyLocator, + asset.ID(sqlDeposit.AssetID), uint32(sqlDeposit.Expiry), + addressParams, + ) + if err != nil { + return Deposit{}, err + } + + return Deposit{ + Kit: kit, + DepositInfo: depositInfo, + }, nil +} diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 9de22a865..8de60e24a 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -67,6 +67,86 @@ func (q *Queries) AddAssetDeposit(ctx context.Context, arg AddAssetDepositParams return err } +const getAssetDeposits = `-- name: GetAssetDeposits :many +SELECT d.deposit_id, d.protocol_version, d.created_at, d.asset_id, d.amount, d.client_script_pubkey, d.server_script_pubkey, d.client_internal_pubkey, d.server_internal_pubkey, d.server_internal_key, d.expiry, d.client_key_family, d.client_key_index, d.addr, d.confirmation_height, d.outpoint, d.pk_script, d.sweep_addr, u.update_state, u.update_timestamp +FROM asset_deposits d +JOIN asset_deposit_updates u ON u.id = ( + SELECT id + FROM asset_deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 +) +ORDER BY d.created_at ASC +` + +type GetAssetDepositsRow struct { + DepositID string + ProtocolVersion int32 + CreatedAt time.Time + AssetID []byte + Amount int64 + ClientScriptPubkey []byte + ServerScriptPubkey []byte + ClientInternalPubkey []byte + ServerInternalPubkey []byte + ServerInternalKey []byte + Expiry int32 + ClientKeyFamily int32 + ClientKeyIndex int32 + Addr string + ConfirmationHeight sql.NullInt32 + Outpoint sql.NullString + PkScript []byte + SweepAddr sql.NullString + UpdateState int32 + UpdateTimestamp time.Time +} + +func (q *Queries) GetAssetDeposits(ctx context.Context) ([]GetAssetDepositsRow, error) { + rows, err := q.db.QueryContext(ctx, getAssetDeposits) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAssetDepositsRow + for rows.Next() { + var i GetAssetDepositsRow + if err := rows.Scan( + &i.DepositID, + &i.ProtocolVersion, + &i.CreatedAt, + &i.AssetID, + &i.Amount, + &i.ClientScriptPubkey, + &i.ServerScriptPubkey, + &i.ClientInternalPubkey, + &i.ServerInternalPubkey, + &i.ServerInternalKey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Addr, + &i.ConfirmationHeight, + &i.Outpoint, + &i.PkScript, + &i.SweepAddr, + &i.UpdateState, + &i.UpdateTimestamp, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const markDepositConfirmed = `-- name: MarkDepositConfirmed :exec UPDATE asset_deposits SET confirmation_height = $2, outpoint = $3, pk_script = $4 diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 0e72ba01b..86299282c 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -21,6 +21,7 @@ type Querier interface { CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error FetchLiquidityParams(ctx context.Context) ([]byte, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) + GetAssetDeposits(ctx context.Context) ([]GetAssetDepositsRow, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error) GetDeposit(ctx context.Context, depositID []byte) (Deposit, error) diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index dd02d5055..304acee2e 100644 --- a/loopdb/sqlc/queries/asset_deposits.sql +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -27,3 +27,15 @@ INSERT INTO asset_deposit_updates ( UPDATE asset_deposits SET confirmation_height = $2, outpoint = $3, pk_script = $4 WHERE deposit_id = $1; + +-- name: GetAssetDeposits :many +SELECT d.*, u.update_state, u.update_timestamp +FROM asset_deposits d +JOIN asset_deposit_updates u ON u.id = ( + SELECT id + FROM asset_deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 +) +ORDER BY d.created_at ASC; From cfdf0644897fd5a4c5dbda1ee42bd4a9b3f240be Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:35:58 +0200 Subject: [PATCH 14/19] assets+loopdb: handle asset deposit expiry and timeout sweep With this commit, asset deposit expiry is checked on each new block. When a deposit expires, a timeout sweep is published. The deposit state is updated both when the sweep is first published and again upon its confirmation. --- assets/deposit/manager.go | 256 +++++++++++++++++++++++++ assets/deposit/sql_store.go | 17 ++ loopdb/sqlc/asset_deposits.sql.go | 16 ++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 + 5 files changed, 295 insertions(+) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 74dfcebf3..283b9ba42 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -26,6 +26,10 @@ var ( // shutting down and that no further calls should be made to it. ErrManagerShuttingDown = errors.New("asset deposit manager is " + "shutting down") + + // lockExpiration us the expiration time we use for sweep fee + // paying inputs. + lockExpiration = time.Hour * 24 ) // DepositUpdateCallback is a callback that is called when a deposit state is @@ -62,6 +66,10 @@ type Manager struct { // currentHeight is the current block height of the chain. currentHeight uint32 + // pendingSweeps is a map of all pending timeout sweeps. The key is the + // deposit ID. + pendingSweeps map[string]struct{} + // deposits is a map of all active deposits. The key is the deposit ID. deposits map[string]*Deposit @@ -109,6 +117,7 @@ func NewManager(depositServiceClient swapserverrpc.AssetDepositServiceClient, sweeper: sweeper, addressParams: addressParams, deposits: make(map[string]*Deposit), + pendingSweeps: make(map[string]struct{}), subscribers: make(map[string][]DepositUpdateCallback), callEnter: make(chan struct{}), callLeave: make(chan struct{}), @@ -212,6 +221,43 @@ func (m *Manager) criticalError(err error) { // handleBlockEpoch is called when a new block is added to the chain. func (m *Manager) handleBlockEpoch(ctx context.Context, height uint32) error { + for _, d := range m.deposits { + if d.State != StateConfirmed { + continue + } + + log.Debugf("Checking if deposit %v is expired, expiry=%v", d.ID, + d.ConfirmationHeight+d.CsvExpiry) + + if height < d.ConfirmationHeight+d.CsvExpiry { + continue + } + + err := m.handleDepositExpired(ctx, d) + if err != nil { + log.Errorf("Unable to update deposit %v state: %v", + d.ID, err) + + return err + } + } + + // Now publish the timeout sweeps for all expired deposits and also + // move them to the pending sweeps map. + for _, d := range m.deposits { + // TODO(bhandras): republish will insert a new transfer entry in + // tapd, despite the transfer already existing. To avoid that, + // we won't re-publish the timeout sweep for now. + if d.State != StateExpired { + continue + } + + err := m.publishTimeoutSweep(ctx, d) + if err != nil { + return err + } + } + return nil } @@ -635,3 +681,213 @@ func (m *Manager) ListDeposits(ctx context.Context, minConfs, maxConfs uint32) ( return filteredDeposits, nil } + +// handleDepositStateUpdate updates the deposit state in the store and +// notifies all subscribers of the deposit state change. +func (m *Manager) handleDepositExpired(ctx context.Context, d *Deposit) error { + // Generate a new address for the timeout sweep. + rpcTimeoutSweepAddr, err := m.tapClient.NewAddr( + ctx, &taprpc.NewAddrRequest{ + AssetId: d.AssetID[:], + Amt: d.Amount, + }, + ) + if err != nil { + log.Errorf("Unable to create timeout sweep address: %v", err) + + return err + } + + d.State = StateExpired + d.SweepAddr = rpcTimeoutSweepAddr.Encoded + + return m.handleDepositStateUpdate(ctx, d) +} + +// publishTimeoutSweep publishes a timeout sweep for the deposit. As we use the +// same lock ID for the sponsoring inputs, it's possible to republish the sweep +// however it'll create a new transfer entry in tapd, which we want to avoid +// (for now). +func (m *Manager) publishTimeoutSweep(ctx context.Context, d *Deposit) error { + // Start monitoring the sweep unless we're already doing so. + if _, ok := m.pendingSweeps[d.ID]; !ok { + err := m.waitForDepositSpend(ctx, d) + if err != nil { + log.Errorf("Unable to wait for deposit %v spend: %v", + d.ID, err) + + return err + } + + m.pendingSweeps[d.ID] = struct{}{} + } + + log.Infof("(Re)publishing timeout sweep for deposit %v", d.ID) + + // TODO(bhandras): conf target should be dynamic/configrable. + const confTarget = 2 + feeRateSatPerKw, err := m.walletKit.EstimateFeeRate( + ctx, confTarget, + ) + + lockID, err := d.lockID() + if err != nil { + return err + } + + sweepAddr, err := address.DecodeAddress(d.SweepAddr, &m.addressParams) + if err != nil { + log.Errorf("Unable to decode timeout sweep address: %v", err) + + return err + } + + snedResp, err := m.sweeper.PublishDepositTimeoutSweep( + ctx, d.Kit, d.Proof, sweepAddr, feeRateSatPerKw.FeePerVByte(), + lockID, lockExpiration, + ) + if err != nil { + // TOOD(bhandras): handle republish errors. + log.Infof("Unable to publish timeout sweep for deposit %v: %v", + d.ID, err) + } else { + log.Infof("Published timeout sweep for deposit %v: %x", d.ID, + snedResp.Transfer.AnchorTxHash) + + // Update deposit state on first successful publish. + if d.State != StateTimeoutSweepPublished { + d.State = StateTimeoutSweepPublished + + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + log.Errorf("Unable to update deposit %v "+ + "state: %v", d.ID, err) + + return err + } + } + } + + return nil +} + +// waitForDepositSpend waits for the deposit to be spent. It subscribes to +// receive events for the deposit's sweep address notifying us once the transfer +// has completed. +func (m *Manager) waitForDepositSpend(ctx context.Context, d *Deposit) error { + log.Infof("Waiting for deposit spend: %s, sweep_addr=%v, created_at=%v", + d.ID, d.SweepAddr, d.CreatedAt) + + resChan, errChan, err := m.tapClient.WaitForReceiveComplete( + ctx, d.SweepAddr, d.CreatedAt, + ) + + if err != nil { + log.Errorf("unable to subscribe to receive events: %v", err) + + return err + } + + go func() { + select { + case res := <-resChan: + // At this point we can consider the deposit confirmed. + err = m.handleDepositSpend( + ctx, d, res.Outpoint.String(), + ) + if err != nil { + m.criticalError(err) + } + + case err := <-errChan: + m.criticalError(err) + } + }() + + return nil +} + +// handleDepositSpend is called when the deposit is spent. It updates the +// deposit state and releases the inputs used for the deposit sweep. +func (m *Manager) handleDepositSpend(ctx context.Context, d *Deposit, + outpoint string) error { + + done, err := m.scheduleNextCall() + if err != nil { + log.Errorf("Unable to schedule next call: %v", err) + + return err + } + defer done() + + switch d.State { + case StateTimeoutSweepPublished: + log.Infof("Deposit %s withdrawn in: %s", d.ID, outpoint) + d.State = StateSwept + + err := m.releaseDepositSweepInputs(ctx, d) + if err != nil { + log.Errorf("Unable to release deposit sweep inputs: "+ + "%v", err) + + return err + } + + default: + err := fmt.Errorf("Spent deposit %s in unexpected state %s", + d.ID, d.State) + + log.Errorf(err.Error()) + + return err + } + + // TODO(bhandras): should save the spend details to the store? + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + log.Errorf("Unable to update deposit %v state: %v", d.ID, err) + + return err + } + + // Sanity check that the deposit is in the pending sweeps map. + if _, ok := m.pendingSweeps[d.ID]; !ok { + log.Errorf("Deposit %v not found in pending deposits", d.ID) + } + + // We can now remove the deposit from the pending sweeps map as we don't + // need to monitor for the spend anymore. + delete(m.pendingSweeps, d.ID) + + return nil +} + +// releaseDepositSweepInputs releases the inputs that were used for the deposit +// sweep. +func (m *Manager) releaseDepositSweepInputs(ctx context.Context, + d *Deposit) error { + + lockID, err := d.lockID() + if err != nil { + return err + } + + leases, err := m.walletKit.ListLeases(ctx) + if err != nil { + return err + } + + for _, lease := range leases { + if lease.LockID != lockID { + continue + } + + // Unlock any UTXOs that were used for the deposit sweep. + err = m.walletKit.ReleaseOutput(ctx, lockID, lease.Outpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index 93d904e7c..52def1107 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -29,6 +29,9 @@ type Querier interface { GetAssetDeposits(ctx context.Context) ([]sqlc.GetAssetDepositsRow, error) + + SetAssetDepositSweepAddr(ctx context.Context, + arg sqlc.SetAssetDepositSweepAddrParams) error } // DepositBaseDB is the interface that contains all the queries generated @@ -127,6 +130,20 @@ func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { if err != nil { return err } + + case StateExpired: + err := tx.SetAssetDepositSweepAddr( + ctx, sqlc.SetAssetDepositSweepAddrParams{ + DepositID: d.ID, + SweepAddr: sql.NullString{ + String: d.SweepAddr, + Valid: true, + }, + }, + ) + if err != nil { + return err + } } return tx.UpdateDepositState( diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 8de60e24a..4de761cdc 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -170,6 +170,22 @@ func (q *Queries) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfi return err } +const setAssetDepositSweepAddr = `-- name: SetAssetDepositSweepAddr :exec +UPDATE asset_deposits +SET sweep_addr = $2 +WHERE deposit_id = $1 +` + +type SetAssetDepositSweepAddrParams struct { + DepositID string + SweepAddr sql.NullString +} + +func (q *Queries) SetAssetDepositSweepAddr(ctx context.Context, arg SetAssetDepositSweepAddrParams) error { + _, err := q.db.ExecContext(ctx, setAssetDepositSweepAddr, arg.DepositID, arg.SweepAddr) + return err +} + const updateDepositState = `-- name: UpdateDepositState :exec INSERT INTO asset_deposit_updates ( deposit_id, diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 86299282c..d01b43d1c 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -65,6 +65,7 @@ type Querier interface { IsStored(ctx context.Context, swapHash []byte) (bool, error) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + SetAssetDepositSweepAddr(ctx context.Context, arg SetAssetDepositSweepAddrParams) error UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error UpdateDepositState(ctx context.Context, arg UpdateDepositStateParams) error diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index 304acee2e..e34c4b45e 100644 --- a/loopdb/sqlc/queries/asset_deposits.sql +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -39,3 +39,8 @@ JOIN asset_deposit_updates u ON u.id = ( LIMIT 1 ) ORDER BY d.created_at ASC; + +-- name: SetAssetDepositSweepAddr :exec +UPDATE asset_deposits +SET sweep_addr = $2 +WHERE deposit_id = $1; From ad9074d270115963af2949df527ffe5834cc0307 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:41:33 +0200 Subject: [PATCH 15/19] assets+loopdb: recover active deposits on startup This commit adds deposit recovery to the deposit manager's startup process. All deposits that have not yet been spent by the server or swept by the client are recovered from the store. --- assets/deposit/manager.go | 49 +++++++++++++++ assets/deposit/sql_store.go | 27 +++++++++ loopdb/sqlc/asset_deposits.sql.go | 82 ++++++++++++++++++++++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 15 +++++ 5 files changed, 174 insertions(+) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 283b9ba42..90fd54659 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -146,6 +146,13 @@ func (m *Manager) Run(ctx context.Context, bestBlock uint32) error { m.currentHeight = bestBlock + err := m.recoverDeposits(ctx) + if err != nil { + log.Errorf("Unable to recover deposits: %v", err) + + return err + } + blockChan, blockErrChan, err := m.chainNotifier.RegisterBlockEpochNtfn( ctxc, ) @@ -219,6 +226,48 @@ func (m *Manager) criticalError(err error) { } } +// recoverDeposits recovers all active deppsits when the deposit manager starts. +func (m *Manager) recoverDeposits(ctx context.Context) error { + // Fetch all active deposits from the store to kick-off the manager. + activeDeposits, err := m.store.GetActiveDeposits(ctx) + if err != nil { + log.Errorf("Unable to fetch deposits from store: %v", err) + + return err + } + + for i := range activeDeposits { + d := &activeDeposits[i] + log.Infof("Recovering deposit %v (state=%s)", d.ID, d.State) + + m.deposits[d.ID] = d + _, _, _, err = m.isDepositFunded(ctx, d) + if err != nil { + return err + } + + if d.State == StateInitiated { + // If the deposit has just been initiated, then we need + // to ensure that it is funded. + err = m.fundDepositIfNeeded(ctx, d) + if err != nil { + log.Errorf("Unable to fund deposit %v: %v", + d.ID, err) + + return err + } + } else { + // Cache proof info of the deposit in-memory. + err = m.cacheProofInfo(ctx, d) + if err != nil { + return err + } + } + } + + return nil +} + // handleBlockEpoch is called when a new block is added to the chain. func (m *Manager) handleBlockEpoch(ctx context.Context, height uint32) error { for _, d := range m.deposits { diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index 52def1107..ff6a6f241 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -32,6 +32,9 @@ type Querier interface { SetAssetDepositSweepAddr(ctx context.Context, arg sqlc.SetAssetDepositSweepAddrParams) error + + GetActiveAssetDeposits(ctx context.Context) ( + []sqlc.GetActiveAssetDepositsRow, error) } // DepositBaseDB is the interface that contains all the queries generated @@ -267,3 +270,27 @@ func sqlcDepositToDeposit(sqlDeposit sqlc.GetAssetDepositsRow, DepositInfo: depositInfo, }, nil } + +// GetActiveDeposits returns all active deposits from the database. Active +// deposits are those that have not yet been spent or swept. +func (s *SQLStore) GetActiveDeposits(ctx context.Context) ([]Deposit, error) { + sqlDeposits, err := s.db.GetActiveAssetDeposits(ctx) + if err != nil { + return nil, err + } + + deposits := make([]Deposit, 0, len(sqlDeposits)) + for _, sqlDeposit := range sqlDeposits { + deposit, err := sqlcDepositToDeposit( + sqlc.GetAssetDepositsRow(sqlDeposit), + &s.addressParams, + ) + if err != nil { + return nil, err + } + + deposits = append(deposits, deposit) + } + + return deposits, nil +} diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 4de761cdc..3364a4dbe 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -67,6 +67,88 @@ func (q *Queries) AddAssetDeposit(ctx context.Context, arg AddAssetDepositParams return err } +const getActiveAssetDeposits = `-- name: GetActiveAssetDeposits :many +SELECT d.deposit_id, d.protocol_version, d.created_at, d.asset_id, d.amount, d.client_script_pubkey, d.server_script_pubkey, d.client_internal_pubkey, d.server_internal_pubkey, d.server_internal_key, d.expiry, d.client_key_family, d.client_key_index, d.addr, d.confirmation_height, d.outpoint, d.pk_script, d.sweep_addr, u.update_state, u.update_timestamp +FROM asset_deposits d +JOIN asset_deposit_updates u + ON u.deposit_id = d.deposit_id +WHERE u.id = ( + SELECT id + FROM asset_deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 +) +AND u.update_state IN (0, 1, 2, 3, 4, 5, 6) +` + +type GetActiveAssetDepositsRow struct { + DepositID string + ProtocolVersion int32 + CreatedAt time.Time + AssetID []byte + Amount int64 + ClientScriptPubkey []byte + ServerScriptPubkey []byte + ClientInternalPubkey []byte + ServerInternalPubkey []byte + ServerInternalKey []byte + Expiry int32 + ClientKeyFamily int32 + ClientKeyIndex int32 + Addr string + ConfirmationHeight sql.NullInt32 + Outpoint sql.NullString + PkScript []byte + SweepAddr sql.NullString + UpdateState int32 + UpdateTimestamp time.Time +} + +func (q *Queries) GetActiveAssetDeposits(ctx context.Context) ([]GetActiveAssetDepositsRow, error) { + rows, err := q.db.QueryContext(ctx, getActiveAssetDeposits) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetActiveAssetDepositsRow + for rows.Next() { + var i GetActiveAssetDepositsRow + if err := rows.Scan( + &i.DepositID, + &i.ProtocolVersion, + &i.CreatedAt, + &i.AssetID, + &i.Amount, + &i.ClientScriptPubkey, + &i.ServerScriptPubkey, + &i.ClientInternalPubkey, + &i.ServerInternalPubkey, + &i.ServerInternalKey, + &i.Expiry, + &i.ClientKeyFamily, + &i.ClientKeyIndex, + &i.Addr, + &i.ConfirmationHeight, + &i.Outpoint, + &i.PkScript, + &i.SweepAddr, + &i.UpdateState, + &i.UpdateTimestamp, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getAssetDeposits = `-- name: GetAssetDeposits :many SELECT d.deposit_id, d.protocol_version, d.created_at, d.asset_id, d.amount, d.client_script_pubkey, d.server_script_pubkey, d.client_internal_pubkey, d.server_internal_pubkey, d.server_internal_key, d.expiry, d.client_key_family, d.client_key_index, d.addr, d.confirmation_height, d.outpoint, d.pk_script, d.sweep_addr, u.update_state, u.update_timestamp FROM asset_deposits d diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index d01b43d1c..cbc7de20c 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -20,6 +20,7 @@ type Querier interface { CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error FetchLiquidityParams(ctx context.Context) ([]byte, error) + GetActiveAssetDeposits(ctx context.Context) ([]GetActiveAssetDepositsRow, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) GetAssetDeposits(ctx context.Context) ([]GetAssetDepositsRow, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index e34c4b45e..22ac1223f 100644 --- a/loopdb/sqlc/queries/asset_deposits.sql +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -44,3 +44,18 @@ ORDER BY d.created_at ASC; UPDATE asset_deposits SET sweep_addr = $2 WHERE deposit_id = $1; + +-- name: GetActiveAssetDeposits :many +SELECT d.*, u.update_state, u.update_timestamp +FROM asset_deposits d +JOIN asset_deposit_updates u + ON u.deposit_id = d.deposit_id +WHERE u.id = ( + SELECT id + FROM asset_deposit_updates + WHERE deposit_id = d.deposit_id + ORDER BY update_timestamp DESC + LIMIT 1 +) +AND u.update_state IN (0, 1, 2, 3, 4, 5, 6); + From c90c45f2b91dd510138eac3e5ab3370191536fb7 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:48:05 +0200 Subject: [PATCH 16/19] assets+loopdb: implement asset deposit withdrawal This commit implements the necessary plumbing to enable deposit withdrawal. Withdrawing reveals the server's internal key for the deposit, allowing the client to sweep it independently. --- assets/deposit/manager.go | 76 ++++++++++++++++++++++++++ assets/deposit/server.go | 16 +++++- assets/deposit/sql_store.go | 18 ++++++ loopdb/sqlc/asset_deposits.sql.go | 17 ++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 ++ 6 files changed, 132 insertions(+), 1 deletion(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 90fd54659..4e3da974d 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -940,3 +940,79 @@ func (m *Manager) releaseDepositSweepInputs(ctx context.Context, return nil } + +// WithdrawDeposits withdraws the deposits with the given IDs. It will first ask +// the server for the deposit keys, then initate the withdrawal by updating the +// deposit state. +func (m *Manager) WithdrawDeposits(ctx context.Context, + depositIDs []string) error { + + done, err := m.scheduleNextCall() + if err != nil { + return err + } + defer done() + + for _, depositID := range depositIDs { + d, ok := m.deposits[depositID] + if !ok { + return fmt.Errorf("deposit %v not found", depositID) + } + + if d.State != StateConfirmed { + return fmt.Errorf("deposit %v is not withdrawable, "+ + "current state: %v", depositID, d.State) + } + + log.Infof("Initiating deposit withdrawal %v: %v", + depositID, d.Amount) + } + + keys, err := m.depositServiceClient.WithdrawAssetDeposits( + ctx, &swapserverrpc.WithdrawAssetDepositsServerReq{ + DepositIds: depositIDs, + }, + ) + if err != nil { + return fmt.Errorf("unable to request withdrawal: %w", err) + } + + for depositID, privKeyBytes := range keys.DepositKeys { + d, ok := m.deposits[depositID] + if !ok { + log.Warnf("Skipping withdrawal of unknown deposit: %v", + depositID) + continue + } + + privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes) + if !d.CoSignerInternalKey.IsEqual(pubKey) { + return fmt.Errorf("revealed co-signer internal key "+ + "does not match local key for %v", depositID) + } + + err := m.store.SetAssetDepositServerKey(ctx, depositID, privKey) + if err != nil { + return err + } + + rpcSweepAddr, err := m.tapClient.NewAddr( + ctx, &taprpc.NewAddrRequest{ + AssetId: d.AssetID[:], + Amt: d.Amount, + }, + ) + if err != nil { + return err + } + + d.State = StateWithdrawn + d.SweepAddr = rpcSweepAddr.Encoded + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + return err + } + } + + return nil +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 3bc1aaaab..816187fd7 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -138,7 +138,21 @@ func (s *Server) WithdrawAssetDeposits(ctx context.Context, in *looprpc.WithdrawAssetDepositsRequest) ( *looprpc.WithdrawAssetDepositsResponse, error) { - return nil, status.Error(codes.Unimplemented, "unimplemented") + if s.manager == nil { + return nil, ErrAssetDepositsUnavailable + } + + if len(in.DepositIds) == 0 { + return nil, status.Error(codes.InvalidArgument, + "at least one deposit id must be provided") + } + + err := s.manager.WithdrawDeposits(ctx, in.DepositIds) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &looprpc.WithdrawAssetDepositsResponse{}, nil } // TestCoSignAssetDepositHTLC is the rpc endpoint for loop clients to test diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index ff6a6f241..a9f181918 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -35,6 +35,9 @@ type Querier interface { GetActiveAssetDeposits(ctx context.Context) ( []sqlc.GetActiveAssetDepositsRow, error) + + SetAssetDepositServerInternalKey(ctx context.Context, + arg sqlc.SetAssetDepositServerInternalKeyParams) error } // DepositBaseDB is the interface that contains all the queries generated @@ -135,6 +138,8 @@ func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { } case StateExpired: + fallthrough + case StateWithdrawn: err := tx.SetAssetDepositSweepAddr( ctx, sqlc.SetAssetDepositSweepAddrParams{ DepositID: d.ID, @@ -294,3 +299,16 @@ func (s *SQLStore) GetActiveDeposits(ctx context.Context) ([]Deposit, error) { return deposits, nil } + +// SetAssetDepositServerKey sets the server's internal key for the give asset +// deposit. +func (s *SQLStore) SetAssetDepositServerKey(ctx context.Context, + depositID string, key *btcec.PrivateKey) error { + + return s.db.SetAssetDepositServerInternalKey( + ctx, sqlc.SetAssetDepositServerInternalKeyParams{ + DepositID: depositID, + ServerInternalKey: key.Serialize(), + }, + ) +} diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 3364a4dbe..032ffe2eb 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -252,6 +252,23 @@ func (q *Queries) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfi return err } +const setAssetDepositServerInternalKey = `-- name: SetAssetDepositServerInternalKey :exec +UPDATE asset_deposits +SET server_internal_key = $2 +WHERE deposit_id = $1 +AND server_internal_key IS NULL +` + +type SetAssetDepositServerInternalKeyParams struct { + DepositID string + ServerInternalKey []byte +} + +func (q *Queries) SetAssetDepositServerInternalKey(ctx context.Context, arg SetAssetDepositServerInternalKeyParams) error { + _, err := q.db.ExecContext(ctx, setAssetDepositServerInternalKey, arg.DepositID, arg.ServerInternalKey) + return err +} + const setAssetDepositSweepAddr = `-- name: SetAssetDepositSweepAddr :exec UPDATE asset_deposits SET sweep_addr = $2 diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index cbc7de20c..879637f05 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -66,6 +66,7 @@ type Querier interface { IsStored(ctx context.Context, swapHash []byte) (bool, error) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + SetAssetDepositServerInternalKey(ctx context.Context, arg SetAssetDepositServerInternalKeyParams) error SetAssetDepositSweepAddr(ctx context.Context, arg SetAssetDepositSweepAddrParams) error UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index 22ac1223f..6a11879d1 100644 --- a/loopdb/sqlc/queries/asset_deposits.sql +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -59,3 +59,8 @@ WHERE u.id = ( ) AND u.update_state IN (0, 1, 2, 3, 4, 5, 6); +-- name: SetAssetDepositServerInternalKey :exec +UPDATE asset_deposits +SET server_internal_key = $2 +WHERE deposit_id = $1 +AND server_internal_key IS NULL; From 77b9bcda6844be9a17fa3e56d2c15ab3edead56a Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:55:15 +0200 Subject: [PATCH 17/19] assets+loopdb: publish cooperative deposit withdrawal transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements the full cooperative deposit withdrawal flow. The client first fetches keys for any pending withdrawals, then publishes sweep transactions using the revealed key to sign the deposit sweep. Once the sweep confirms, the deposit’s state is updated in the deposit store. --- assets/deposit/manager.go | 89 ++++++++++++++++++++ assets/deposit/sql_store.go | 19 +++++ go.mod | 57 +++++++++---- go.sum | 111 +++++++++++++------------ loopdb/sqlc/asset_deposits.sql.go | 13 +++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 ++ 7 files changed, 224 insertions(+), 71 deletions(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 4e3da974d..156c7c5e5 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -163,6 +163,11 @@ func (m *Manager) Run(ctx context.Context, bestBlock uint32) error { return err } + // Wake the manager up very 10 seconds to check if there're any pending + // chores to do. + const wakeupInterval = time.Duration(10) * time.Second + withdrawTicker := time.NewTicker(wakeupInterval) + for { select { case <-m.callEnter: @@ -182,6 +187,15 @@ func (m *Manager) Run(ctx context.Context, bestBlock uint32) error { return err } + case <-withdrawTicker.C: + err := m.publishPendingWithdrawals(ctx) + if err != nil { + log.Errorf("Unable to publish pending "+ + "withdrawals: %v", err) + + return err + } + case err := <-blockErrChan: log.Errorf("received error from block epoch "+ "notification: %v", err) @@ -871,6 +885,8 @@ func (m *Manager) handleDepositSpend(ctx context.Context, d *Deposit, switch d.State { case StateTimeoutSweepPublished: + fallthrough + case StateCooperativeSweepPublished: log.Infof("Deposit %s withdrawn in: %s", d.ID, outpoint) d.State = StateSwept @@ -1016,3 +1032,76 @@ func (m *Manager) WithdrawDeposits(ctx context.Context, return nil } + +// publishPendingWithdrawals publishes any pending deposit withdrawals. +func (m *Manager) publishPendingWithdrawals(ctx context.Context) error { + for _, d := range m.deposits { + // TODO(bhandras): republish on StateCooperativeSweepPublished. + if d.State != StateWithdrawn { + continue + } + + // Start monitoring the sweep unless we're already doing so. + if _, ok := m.pendingSweeps[d.ID]; !ok { + err := m.waitForDepositSpend(ctx, d) + if err != nil { + log.Errorf("Unable to wait for deposit %v "+ + "spend: %v", d.ID, err) + + return err + } + + m.pendingSweeps[d.ID] = struct{}{} + } + + serverKey, err := m.store.GetAssetDepositServerKey( + ctx, d.ID, + ) + if err != nil { + return err + } + + lockID, err := d.lockID() + if err != nil { + return err + } + + sweepAddr, err := address.DecodeAddress( + d.SweepAddr, &m.addressParams, + ) + if err != nil { + return err + } + + // TODO(bhandras): conf target should be dynamic/configrable. + const confTarget = 2 + feeRateSatPerKw, err := m.walletKit.EstimateFeeRate( + ctx, confTarget, + ) + + funder := true + sendAssetResp, err := m.sweeper.PublishDepositSweepMuSig2( + ctx, d.Kit, funder, d.Proof, serverKey, sweepAddr, + feeRateSatPerKw.FeePerVByte(), lockID, lockExpiration, + ) + if err != nil { + log.Errorf("Unable to publish deposit sweep for %v: %v", + d.ID, err) + } else { + log.Infof("Published sweep for deposit %v: %v", d.ID, + sendAssetResp.Transfer.AnchorTxHash) + + d.State = StateCooperativeSweepPublished + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + log.Errorf("Unable to update deposit %v "+ + "state: %v", d.ID, err) + + return err + } + + } + } + + return nil +} diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index a9f181918..8772a395d 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -38,6 +38,9 @@ type Querier interface { SetAssetDepositServerInternalKey(ctx context.Context, arg sqlc.SetAssetDepositServerInternalKeyParams) error + + GetAssetDepositServerInternalKey(ctx context.Context, + depositID string) ([]byte, error) } // DepositBaseDB is the interface that contains all the queries generated @@ -312,3 +315,19 @@ func (s *SQLStore) SetAssetDepositServerKey(ctx context.Context, }, ) } + +func (s *SQLStore) GetAssetDepositServerKey(ctx context.Context, + depositID string) (*btcec.PrivateKey, error) { + + keyBytes, err := s.db.GetAssetDepositServerInternalKey(ctx, depositID) + if err != nil { + return nil, err + } + + key, _ := btcec.PrivKeyFromBytes(keyBytes) + if err != nil { + return nil, err + } + + return key, nil +} diff --git a/go.mod b/go.mod index 97a54488c..754d100d6 100644 --- a/go.mod +++ b/go.mod @@ -29,13 +29,13 @@ require ( github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/queue v1.1.1 github.com/lightningnetwork/lnd/ticker v1.1.1 - github.com/lightningnetwork/lnd/tlv v1.3.1 + github.com/lightningnetwork/lnd/tlv v1.3.2 github.com/lightningnetwork/lnd/tor v1.1.6 github.com/ory/dockertest/v3 v3.10.0 github.com/stretchr/testify v1.10.0 github.com/urfave/cli v1.22.14 go.etcd.io/bbolt v1.3.11 - golang.org/x/sync v0.12.0 + golang.org/x/sync v0.13.0 google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.34.2 gopkg.in/macaroon-bakery.v2 v2.3.0 @@ -72,8 +72,8 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect - github.com/docker/cli v28.0.1+incompatible // indirect - github.com/docker/docker v28.0.1+incompatible // indirect + github.com/docker/cli v28.1.1+incompatible // indirect + github.com/docker/docker v28.1.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -101,12 +101,12 @@ require ( github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgtype v1.14.0 // indirect - github.com/jackc/pgx/v4 v4.18.2 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.4 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect github.com/jackc/puddle v1.3.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/gateway v1.0.5 // indirect github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad // indirect github.com/jonboulle/clockwork v0.2.2 // indirect @@ -120,11 +120,11 @@ require ( github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3 // indirect github.com/lightninglabs/neutrino v0.16.1 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect - github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect + github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.8 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect - github.com/lightningnetwork/lnd/sqldb v1.0.9 // indirect + github.com/lightningnetwork/lnd/sqldb v1.0.10 // indirect github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect @@ -180,13 +180,13 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect @@ -215,4 +215,27 @@ replace github.com/lightninglabs/loop/swapserverrpc => ./swapserverrpc replace github.com/lightninglabs/loop/looprpc => ./looprpc -go 1.23.9 +// Temporary replace to add SubmitPackage support (https://github.com/btcsuite/btcwallet/pull/1009). +replace github.com/btcsuite/btcwallet => github.com/bhandras/btcwallet v0.11.1-0.20250507171803-0de1c46b1cfc + +// Temporary replace to add SubmitPackage support to btcd (https://github.com/btcsuite/btcd/pull/2366). +replace github.com/btcsuite/btcd => github.com/bhandras/btcd v0.22.0-beta.0.20250507171227-f18160c86e92 + +// Temporary replace to add SubmitPackage support to lnd (https://github.com/lightningnetwork/lnd/pull/9784). +replace github.com/lightningnetwork/lnd => github.com/bhandras/lnd v0.8.0-beta-rc3.0.20250717123715-6cda96a60994 + +// Temporary replace to make lnd compile with the SubmitPackage changes. +replace github.com/lightningnetwork/lnd/sqldb => github.com/bhandras/lnd/sqldb v0.0.0-20250716041958-643fbb8af65b + +// Temporary replace to include client API for SubmitPackage in lndclient (https://github.com/lightninglabs/lndclient/pull/223) +replace github.com/lightninglabs/lndclient => github.com/lightninglabs/lndclient v1.0.1-0.20250717123354-cf534c9968b9 + +// Temporary replace to experimentally change all bitcoin transactions to use v3 (https://github.com/bhandras/taproot-assets/tree/v3-experimental) +replace github.com/lightninglabs/taproot-assets => github.com/bhandras/taproot-assets v0.0.0-20250717122952-ca46f25f6c3b + +// Temporary replace to make taproot-assets compile with the v3 changes. +replace github.com/lightninglabs/taproot-assets/taprpc => github.com/bhandras/taproot-assets/taprpc v0.0.0-20250605133854-360a25355248 + +go 1.23.10 + +toolchain go1.24.5 diff --git a/go.sum b/go.sum index 97271d03d..baec250bf 100644 --- a/go.sum +++ b/go.sum @@ -642,19 +642,23 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bhandras/btcd v0.22.0-beta.0.20250507171227-f18160c86e92 h1:/H5Dv5VoKqsXkI3iZc6D3xonibJS6YdPHgAa2XkFHi0= +github.com/bhandras/btcd v0.22.0-beta.0.20250507171227-f18160c86e92/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/bhandras/btcwallet v0.11.1-0.20250507171803-0de1c46b1cfc h1:RvT6udxYM857Kvj5fEkWhTo0wAT0t7R7oOgYSoLJOLY= +github.com/bhandras/btcwallet v0.11.1-0.20250507171803-0de1c46b1cfc/go.mod h1:PZ4WgE93vP5TBchtfrlvf5GT6P9ul0tM8rTH1BSYloo= +github.com/bhandras/lnd v0.8.0-beta-rc3.0.20250717123715-6cda96a60994 h1:61nMss7Syk1g1oO6c8vMP/YSthfi0HUfoaGyt+0JR5Y= +github.com/bhandras/lnd v0.8.0-beta-rc3.0.20250717123715-6cda96a60994/go.mod h1:uq19F2JuEISti9T23gfPWkeNFL9O1lre5g/OBTc9mRo= +github.com/bhandras/lnd/sqldb v0.0.0-20250716041958-643fbb8af65b h1:Eb1tarG9pgXfyYVWo2HzZU30W3SMd7L/6T7dFdviGIU= +github.com/bhandras/lnd/sqldb v0.0.0-20250716041958-643fbb8af65b/go.mod h1:JrbvoQOUPXN1Mazh/KRi1LZf0kfGZsH8OY3mF3niqS8= +github.com/bhandras/taproot-assets v0.0.0-20250717122952-ca46f25f6c3b h1:240ExplnjMADe53beoi9MKmj+8O85GuKzwuuHso3C0Y= +github.com/bhandras/taproot-assets v0.0.0-20250717122952-ca46f25f6c3b/go.mod h1:rF+GwuUVuDVUejAHsUCml4Nru9xnl7A4YZQfR4qLMzY= +github.com/bhandras/taproot-assets/taprpc v0.0.0-20250605133854-360a25355248 h1:6YNvikjQFxksYtq/yutSrX6sRhsEa+jeAR+fGuyCtqQ= +github.com/bhandras/taproot-assets/taprpc v0.0.0-20250605133854-360a25355248/go.mod h1:vOM2Ap2wYhEZjiJU7bNNg+e5tDxkvRAuyXwf/KQ4tgo= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= -github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= -github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= -github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= -github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= -github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= github.com/btcsuite/btcd/btcutil/psbt v1.1.10 h1:TC1zhxhFfhnGqoPjsrlEpoqzh+9TPOHrCgnPR47Mj9I= @@ -668,9 +672,6 @@ github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzg github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084 h1:y3bvkt8ki0KX35eUEU8XShRHusz1S+55QwXUTmxn888= github.com/btcsuite/btclog/v2 v2.0.1-0.20250602222548-9967d19bb084/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= -github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.14 h1:CofysgmI1ednkLsXontAdBoXJkbiim7unXnFKhLLjnE= -github.com/btcsuite/btcwallet v0.16.14/go.mod h1:H6dfoZcWPonM2wbVsR2ZBY0PKNZKdQyLAmnX8vL9JFA= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= @@ -685,9 +686,7 @@ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JG github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= -github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= @@ -758,10 +757,10 @@ github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs= -github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= -github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k= +github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= +github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -1011,29 +1010,32 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= +github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc= @@ -1106,8 +1108,8 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3 h1:NuDp6Z+QNMSzZ/+RzWsjgAgQSr/REDxTiHmTczZxlXA= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.3/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM= -github.com/lightninglabs/lndclient v0.19.0-12 h1:aSIKfnvnHKiyFWppUGHJG5fn8VoF5WG5Lx958ksLmqs= -github.com/lightninglabs/lndclient v0.19.0-12/go.mod h1:cicoJY1AwZuRVXGD8Knp50TRT7TGBmw1k37uPQsGQiw= +github.com/lightninglabs/lndclient v1.0.1-0.20250717123354-cf534c9968b9 h1:W0fnqItIl0OeyZYfIphj13Sfer5Q3pTE0PJjR/MUEfg= +github.com/lightninglabs/lndclient v1.0.1-0.20250717123354-cf534c9968b9/go.mod h1:aven9VZtddSIXC1IH60wo+vG0EEfnZsmOgDV+yQvIKo= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= @@ -1116,14 +1118,8 @@ github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3 github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.6.1 h1:98XCk7nvAridyE67uct0NDVpyY1evpIdvPQpeNElskM= -github.com/lightninglabs/taproot-assets v0.6.1/go.mod h1:rF+GwuUVuDVUejAHsUCml4Nru9xnl7A4YZQfR4qLMzY= -github.com/lightninglabs/taproot-assets/taprpc v1.0.8-0.20250617163017-cf2a5e5bb47c h1:Fzob+kYq68uPuaEd78rVa/Jvjn/Rp4rLcG7xRTOxK7Y= -github.com/lightninglabs/taproot-assets/taprpc v1.0.8-0.20250617163017-cf2a5e5bb47c/go.mod h1:vOM2Ap2wYhEZjiJU7bNNg+e5tDxkvRAuyXwf/KQ4tgo= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.2-beta h1:3SKVrKYFY4IJLlrMf7cDzZcBeT+MxjI9Xy6YpY+EEX4= -github.com/lightningnetwork/lnd v0.19.2-beta/go.mod h1:+yKUfIGKKYRHGewgzQ6xi0S26DIfBiMv1zCdB3m6YxA= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= +github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= @@ -1136,12 +1132,10 @@ github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.9 h1:7OHi+Hui823mB/U9NzCdlZTAGSVdDCbjp33+6d/Q+G0= -github.com/lightningnetwork/lnd/sqldb v1.0.9/go.mod h1:OG09zL/PHPaBJefp4HsPz2YLUJ+zIQHbpgCtLnOx8I4= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= -github.com/lightningnetwork/lnd/tlv v1.3.1 h1:o7CZg06y+rJZfUMAo0WzBLr0pgBWCzrt0f9gpujYUzk= -github.com/lightningnetwork/lnd/tlv v1.3.1/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= +github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= +github.com/lightningnetwork/lnd/tlv v1.3.2/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= github.com/lightningnetwork/lnd/tor v1.1.6 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE= github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= @@ -1191,13 +1185,10 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= @@ -1425,8 +1416,11 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1489,7 +1483,6 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1558,8 +1551,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1606,8 +1602,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1707,8 +1703,11 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1718,8 +1717,11 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1736,8 +1738,9 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 032ffe2eb..8bba9c751 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -149,6 +149,19 @@ func (q *Queries) GetActiveAssetDeposits(ctx context.Context) ([]GetActiveAssetD return items, nil } +const getAssetDepositServerInternalKey = `-- name: GetAssetDepositServerInternalKey :one +SELECT server_internal_key +FROM asset_deposits +WHERE deposit_id = $1 +` + +func (q *Queries) GetAssetDepositServerInternalKey(ctx context.Context, depositID string) ([]byte, error) { + row := q.db.QueryRowContext(ctx, getAssetDepositServerInternalKey, depositID) + var server_internal_key []byte + err := row.Scan(&server_internal_key) + return server_internal_key, err +} + const getAssetDeposits = `-- name: GetAssetDeposits :many SELECT d.deposit_id, d.protocol_version, d.created_at, d.asset_id, d.amount, d.client_script_pubkey, d.server_script_pubkey, d.client_internal_pubkey, d.server_internal_pubkey, d.server_internal_key, d.expiry, d.client_key_family, d.client_key_index, d.addr, d.confirmation_height, d.outpoint, d.pk_script, d.sweep_addr, u.update_state, u.update_timestamp FROM asset_deposits d diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 879637f05..2d0a7e043 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -22,6 +22,7 @@ type Querier interface { FetchLiquidityParams(ctx context.Context) ([]byte, error) GetActiveAssetDeposits(ctx context.Context) ([]GetActiveAssetDepositsRow, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) + GetAssetDepositServerInternalKey(ctx context.Context, depositID string) ([]byte, error) GetAssetDeposits(ctx context.Context) ([]GetAssetDepositsRow, error) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error) diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index 6a11879d1..1d761a7e7 100644 --- a/loopdb/sqlc/queries/asset_deposits.sql +++ b/loopdb/sqlc/queries/asset_deposits.sql @@ -64,3 +64,8 @@ UPDATE asset_deposits SET server_internal_key = $2 WHERE deposit_id = $1 AND server_internal_key IS NULL; + +-- name: GetAssetDepositServerInternalKey :one +SELECT server_internal_key +FROM asset_deposits +WHERE deposit_id = $1; From 54ff41119229e0262991913aed57eb787d416193 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 15:00:10 +0200 Subject: [PATCH 18/19] assets: implement asset deposit key reveal With this commit, the client can now reveal the keys of asset deposits to the server. This allows the server to sweep the deposits as needed, without requiring further interaction with the client. --- assets/deposit/manager.go | 73 +++++++++++++++++++++++++++++++++++++++ assets/deposit/server.go | 13 ++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 156c7c5e5..fac211c5b 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -1105,3 +1105,76 @@ func (m *Manager) publishPendingWithdrawals(ctx context.Context) error { return nil } + +// RevealDepositKeys reveals the internal keys for the given deposit IDs to +// the swap server. +func (m *Manager) RevealDepositKeys(ctx context.Context, + depositIDs []string) error { + + done, err := m.scheduleNextCall() + if err != nil { + return err + } + defer done() + + // First check that all requested deposits are in the required state and + // collect the keys. + keys := make(map[string][]byte, len(depositIDs)) + for _, depositID := range depositIDs { + d, ok := m.deposits[depositID] + if !ok { + log.Warnf("Can't reveal key for deposit %v as it is "+ + "not active", depositID) + } + + if d.State != StateConfirmed && d.State != StateKeyRevealed { + return fmt.Errorf("deposit %v key cannot be revealed", + depositID) + } + + internalPubKey, internalPrivKey, err := DeriveSharedDepositKey( + ctx, m.signer, d.FunderScriptKey, + ) + if err != nil { + return err + } + + if !d.FunderInternalKey.IsEqual(internalPubKey) { + log.Errorf("Funder internal key %x does not match "+ + "expected %x for deposit %v", + d.FunderInternalKey.SerializeCompressed(), + internalPubKey.SerializeCompressed(), depositID) + + return fmt.Errorf("funder internal key mismatch") + } + + keys[depositID] = internalPrivKey.Serialize() + } + + // Update the deposit state before we actually push the keys to the + // server. Otherwise we may fail to update the state in our database, + // despite the server accepting the keys. + for depositID := range keys { + d := m.deposits[depositID] + d.State = StateKeyRevealed + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + return err + } + + log.Infof("Revealing deposit key for %v: pub=%x", depositID, + d.FunderInternalKey.SerializeCompressed()) + } + + // Now push the keys to the server. + _, err = m.depositServiceClient.PushAssetDepositKeys( + ctx, &swapserverrpc.PushAssetDepositKeysReq{ + DepositKeys: keys, + }, + ) + if err != nil { + return err + } + + return err +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 816187fd7..675221901 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -129,7 +129,18 @@ func (s *Server) RevealAssetDepositKey(ctx context.Context, in *looprpc.RevealAssetDepositKeyRequest) ( *looprpc.RevealAssetDepositKeyResponse, error) { - return nil, status.Error(codes.Unimplemented, "unimplemented") + if s.manager == nil { + return nil, ErrAssetDepositsUnavailable + } + + err := s.manager.RevealDepositKeys( + ctx, []string{in.DepositId}, + ) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &looprpc.RevealAssetDepositKeyResponse{}, nil } // WithdrawAssetDeposits is the rpc endpoint for loop clients to withdraw their From 1e4c8f1679a0f80af9f2374645aa9acb74c66bb3 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 15:29:25 +0200 Subject: [PATCH 19/19] assets: add support for co-signing a zero-fee deposit HTLC spend This commit extends the deposit manager and sweeper to support generating a zero-fee HTLC transaction that spends a selected deposit. Once the HTLC is prepared, it is partially signed by the client and can be handed to the server as a safety measure before using the deposit in a trustless swap. This typically occurs after the client has accepted the swap payment. If the client becomes unresponsive during the swap process, the server can use the zero-fee HTLC as part of a recovery package. --- assets/deposit/manager.go | 69 +++++++++++++++++++++++ assets/deposit/server.go | 16 +++++- assets/deposit/sweeper.go | 113 ++++++++++++++++++++++++++++++++++++++ assets/deposit/testing.go | 57 +++++++++++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 assets/deposit/testing.go diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index fac211c5b..f2679f121 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -1,20 +1,24 @@ package deposit import ( + "bytes" "context" "errors" "fmt" "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wallet" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/assets" "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightningnetwork/lnd/lntypes" ) var ( @@ -1178,3 +1182,68 @@ func (m *Manager) RevealDepositKeys(ctx context.Context, return err } + +// CoSignHTLC will partially a deposit spending zero-fee HTLC and send the +// resulting signature to the swap server. +func (m *Manager) CoSignHTLC(ctx context.Context, depositID string, + serverNonce [musig2.PubNonceSize]byte, hash lntypes.Hash, + csvExpiry uint32) error { + + done, err := m.scheduleNextCall() + if err != nil { + return err + } + defer done() + + deposit, ok := m.deposits[depositID] + if !ok { + return fmt.Errorf("deposit %v not available", depositID) + } + + _, htlcPkt, _, _, _, err := m.sweeper.GetHTLC( + ctx, deposit.Kit, deposit.Proof, deposit.Amount, hash, + csvExpiry, + ) + if err != nil { + log.Errorf("Unable to get HTLC packet: %v", err) + + return err + } + + prevOutFetcher := wallet.PsbtPrevOutputFetcher(htlcPkt) + sigHash, err := getSigHash(htlcPkt.UnsignedTx, 0, prevOutFetcher) + if err != nil { + return err + } + + nonce, partialSig, err := m.sweeper.PartialSignMuSig2( + ctx, deposit.Kit, deposit.AnchorRootHash, serverNonce, sigHash, + ) + if err != nil { + log.Errorf("Unable to partial sign HTLC: %v", err) + + return err + } + + var pktBuf bytes.Buffer + err = htlcPkt.Serialize(&pktBuf) + if err != nil { + log.Errorf("Unable to write HTLC packet: %v", err) + return err + } + + _, err = m.depositServiceClient.PushAssetDepositHtlcSigs( + ctx, &swapserverrpc.PushAssetDepositHtlcSigsReq{ + PartialSigs: []*swapserverrpc.AssetDepositPartialSig{ + { + DepositId: depositID, + Nonce: nonce[:], + PartialSig: partialSig, + }, + }, + HtlcPsbt: pktBuf.Bytes(), + }, + ) + + return err +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 675221901..c92f44dd9 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -7,6 +7,7 @@ import ( "github.com/lightninglabs/loop/looprpc" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightningnetwork/lnd/lntypes" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -172,5 +173,18 @@ func (s *Server) TestCoSignAssetDepositHTLC(ctx context.Context, in *looprpc.TestCoSignAssetDepositHTLCRequest) ( *looprpc.TestCoSignAssetDepositHTLCResponse, error) { - return nil, status.Error(codes.Unimplemented, "unimplemented") + // Values for testing. + serverNonce := secNonceToPubNonce(tmpServerSecNonce) + preimage := lntypes.Preimage{1, 2, 3} + swapHash := preimage.Hash() + htlcExpiry := uint32(100) + + err := s.manager.CoSignHTLC( + ctx, in.DepositId, serverNonce, swapHash, htlcExpiry, + ) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &looprpc.TestCoSignAssetDepositHTLCResponse{}, nil } diff --git a/assets/deposit/sweeper.go b/assets/deposit/sweeper.go index e6d7192fb..4d8b33607 100644 --- a/assets/deposit/sweeper.go +++ b/assets/deposit/sweeper.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -16,11 +17,16 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/assets" + "github.com/lightninglabs/loop/assets/htlc" "github.com/lightninglabs/loop/utils" "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tappsbt" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -333,3 +339,110 @@ func getSigHash(tx *wire.MsgTx, idx int, return sigHash, nil } + +// GetHTLC creates a new zero-fee HTLC packet to be able to partially sign it +// and send it to the server for further processing. +// +// TODO(bhandras): add support for spending multiple deposits into the HTLC. +func (s *Sweeper) GetHTLC(ctx context.Context, deposit *Kit, + depositProof *proof.Proof, amount uint64, hash lntypes.Hash, + csvExpiry uint32) (*htlc.SwapKit, *psbt.Packet, []*tappsbt.VPacket, + []*tappsbt.VPacket, *assetwalletrpc.CommitVirtualPsbtsResponse, error) { + + // Genearate the HTLC address that will be used to sweep the deposit to + // in case the client is uncooperative. + rpcHtlcAddr, swapKit, err := deposit.NewHtlcAddr( + ctx, s.tapdClient, amount, hash, csvExpiry, + ) + if err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+ + "htlc addr: %v", err) + } + + htlcAddr, err := address.DecodeAddress( + rpcHtlcAddr.Encoded, &s.addressParams, + ) + if err != nil { + return nil, nil, nil, nil, nil, err + } + + // Now we can create the sweep vpacket that'd sweep the deposited + // assets to the HTLC output. + depositSpendVpkt, err := assets.CreateOpTrueSweepVpkt( + ctx, []*proof.Proof{depositProof}, htlcAddr, &s.addressParams, + ) + if err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("unable to create "+ + "deposit spend vpacket: %v", err) + } + + // By committing the virtual transaction to the BTC template we + // created, our lnd node will fund the BTC level transaction with an + // input to pay for the fees. We'll further add a change output to the + // transaction that will be generated using the above key descriptor. + feeRate := chainfee.SatPerVByte(0) + + // Use an empty lock ID, as we don't need to lock any UTXOs for this + // operation. + lockID := wtxmgr.LockID{} + + htlcBtcPkt, activeAssets, passiveAssets, commitResp, err := + s.tapdClient.PrepareAndCommitVirtualPsbts( + ctx, depositSpendVpkt, feeRate, nil, + s.addressParams.Params, nil, &lockID, + time.Duration(0), + ) + if err != nil { + return nil, nil, nil, nil, nil, fmt.Errorf("deposit spend "+ + "HTLC prepare and commit failed: %v", err) + } + + htlcBtcPkt.UnsignedTx.Version = 3 + + return swapKit, htlcBtcPkt, activeAssets, passiveAssets, commitResp, nil +} + +// PartialSignMuSig2 is used to partially sign a message hash with the deposit's +// keys. +func (s *Sweeper) PartialSignMuSig2(ctx context.Context, d *Kit, + anchorRootHash []byte, cosignerNonce [musig2.PubNonceSize]byte, + message [32]byte) ([musig2.PubNonceSize]byte, []byte, error) { + + signers := [][]byte{ + d.FunderInternalKey.SerializeCompressed(), + d.CoSignerInternalKey.SerializeCompressed(), + } + + session, err := s.signer.MuSig2CreateSession( + ctx, input.MuSig2Version100RC2, + &keychain.KeyLocator{ + Family: d.KeyLocator.Family, + Index: d.KeyLocator.Index, + }, signers, lndclient.MuSig2TaprootTweakOpt( + anchorRootHash, false, + ), + ) + if err != nil { + return [musig2.PubNonceSize]byte{}, nil, err + } + + _, err = s.signer.MuSig2RegisterNonces( + ctx, session.SessionID, + [][musig2.PubNonceSize]byte{cosignerNonce}, + ) + if err != nil { + return [musig2.PubNonceSize]byte{}, nil, err + } + + clientPartialSig, err := s.signer.MuSig2Sign( + ctx, session.SessionID, message, true, + ) + if err != nil { + return [musig2.PubNonceSize]byte{}, nil, err + } + + fmt.Printf("!!! client partial sig: %x\n", clientPartialSig) + fmt.Printf("!!! client nonce: %x\n", session.PublicNonce) + + return session.PublicNonce, clientPartialSig, nil +} diff --git a/assets/deposit/testing.go b/assets/deposit/testing.go new file mode 100644 index 000000000..d6e9661a0 --- /dev/null +++ b/assets/deposit/testing.go @@ -0,0 +1,57 @@ +package deposit + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" +) + +var ( + // tmpServerSecNonce is a secret nonce used for testing. + // TODO(bhandras): remove once package spending works e2e. + tmpServerSecNonce = [musig2.SecNonceSize]byte{ + // First 32 bytes: scalar k1 + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0, 0x01, + + // Second 32 bytes: scalar k2 + 0x02, 0x13, 0x24, 0x35, 0x46, 0x57, 0x68, 0x79, + 0x8a, 0x9b, 0xac, 0xbd, 0xce, 0xdf, 0xf1, 0x02, + 0x14, 0x26, 0x38, 0x4a, 0x5c, 0x6e, 0x70, 0x82, + 0x94, 0xa6, 0xb8, 0xca, 0xdc, 0xee, 0xf1, 0x03, + } +) + +// secNonceToPubNonce takes our two secret nonces, and produces their two +// corresponding EC points, serialized in compressed format. +func secNonceToPubNonce( + secNonce [musig2.SecNonceSize]byte) [musig2.PubNonceSize]byte { + + var k1Mod, k2Mod btcec.ModNScalar + k1Mod.SetByteSlice(secNonce[:btcec.PrivKeyBytesLen]) + k2Mod.SetByteSlice(secNonce[btcec.PrivKeyBytesLen:]) + + var r1, r2 btcec.JacobianPoint + btcec.ScalarBaseMultNonConst(&k1Mod, &r1) + btcec.ScalarBaseMultNonConst(&k2Mod, &r2) + + // Next, we'll convert the key in jacobian format to a normal public + // key expressed in affine coordinates. + r1.ToAffine() + r2.ToAffine() + r1Pub := btcec.NewPublicKey(&r1.X, &r1.Y) + r2Pub := btcec.NewPublicKey(&r2.X, &r2.Y) + + var pubNonce [musig2.PubNonceSize]byte + + // The public nonces are serialized as: R1 || R2, where both keys are + // serialized in compressed format. + copy(pubNonce[:], r1Pub.SerializeCompressed()) + copy( + pubNonce[btcec.PubKeyBytesLenCompressed:], + r2Pub.SerializeCompressed(), + ) + + return pubNonce +}