From e72353ad32716acd700329e1bca3f6fefb7cd5f6 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:09:46 +0200 Subject: [PATCH 01/20] 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 | 445 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 533 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..842bbc66d --- /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..335eb20cb --- /dev/null +++ b/assets/htlc/swapkit.go @@ -0,0 +1,445 @@ +package htlc + +import ( + "context" + + "github.com/btcsuite/btcd/btcec/v2" + "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. + Amount uint64 + + // 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) { + sortKeys := true + aggregateKey, err := input.MuSig2CombineKeys( + input.MuSig2Version100RC2, + []*btcec.PublicKey{ + s.SenderPubKey, s.ReceiverPubKey, + }, + sortKeys, + &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{ + AssetVersion: asset.V1, + Amount: s.Amount, + 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(p *proof.Proof) ([]byte, error) { + tapCommitment, err := p.VerifyProofs() + if err != nil { + return nil, err + } + + taprootAssetRoot := tapCommitment.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 6a54be23ae47a4dc4a4a953e238a2e147b0bd9f8 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:18:46 +0200 Subject: [PATCH 02/20] 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 | 30 ++++++++++++++++++++++++------ assets/htlc/swapkit.go | 15 +++++++++++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/assets/htlc/script.go b/assets/htlc/script.go index 842bbc66d..80f6707b9 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, csvOne bool) ([]byte, error) { builder := txscript.NewScriptBuilder() @@ -24,9 +27,22 @@ func GenSuccessPathScript(receiverHtlcKey *btcec.PublicKey, 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) + // OP_EQUAL will leave 0 or 1 on the stack depending on whether the hash + // matches. + // - If it matches and CSV is not used, the script will + // evaulate to true. + // - If it matches and CSV is used, we'll have 1 on the stack which is + // used to verify the CSV condition. + // - If it does not match, we'll have 0 on the stack which will cause + // the script to fail even if CSV is used. + builder.AddOp(txscript.OP_EQUAL) + + if csvOne { + // If csvOne is true, we add a CHECKSEQUENCEVERIFY to ensure + // that the HTLC can only be claimed after at least one + // confirmation. + builder.AddOp(txscript.OP_CHECKSEQUENCEVERIFY) + } return builder.Script() } @@ -61,7 +77,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 335eb20cb..7d86008dd 100644 --- a/assets/htlc/swapkit.go +++ b/assets/htlc/swapkit.go @@ -47,11 +47,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. @@ -192,7 +197,7 @@ func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) ( InternalKey: internalKey, LeafVersion: txscript.BaseLeafVersion, InclusionProof: append( - successLeafHash[:], taprootAssetRoot[:]..., + successLeafHash[:], taprootAssetRoot..., ), } @@ -233,7 +238,7 @@ func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) ( InternalKey: internalKey, LeafVersion: txscript.BaseLeafVersion, InclusionProof: append( - timeOutLeafHash[:], taprootAssetRoot[:]..., + timeOutLeafHash[:], taprootAssetRoot..., ), } @@ -322,7 +327,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 eeb929c0f1529f9e90d6aaa94abb158bd4e6ccb8 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:32:57 +0200 Subject: [PATCH 03/20] 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 | 651 ++++++++++++++++++++++++++++++++++++++++++++--- assets/tapkit.go | 120 +++++++++ 2 files changed, 742 insertions(+), 29 deletions(-) create mode 100644 assets/tapkit.go diff --git a/assets/client.go b/assets/client.go index 88c23efe5..e69136ced 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,436 @@ 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, label string) (*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, + Label: label, + } + + 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 +768,141 @@ 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) + + isComplete := func(event *taprpc.SendEvent) bool { + return event.SendState == + tapfreighter.SendStateComplete.String() } - return conn, nil + go func() { + for { + event, err := sendEventsClient.Recv() + if err != nil { + errChan <- err + + return + } + + if isComplete(event) { + resChan <- TapSendEvent{ + Transfer: event.Transfer, + } + return + } + } + }() + + return resChan, errChan, nil } diff --git a/assets/tapkit.go b/assets/tapkit.go new file mode 100644 index 000000000..fc4338c57 --- /dev/null +++ b/assets/tapkit.go @@ -0,0 +1,120 @@ +package assets + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "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 interactively 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, + sweepScriptKey asset.ScriptKey, sweepInternalKey *btcec.PublicKey, + tapScriptSibling *commitment.TapscriptPreimage, + 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 + } + + sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{ + AssetVersion: asset.V1, + Amount: total, + Interactive: true, + AnchorOutputIndex: 0, + ScriptKey: sweepScriptKey, + AnchorOutputInternalKey: sweepInternalKey, + AnchorOutputTapscriptSibling: tapScriptSibling, + ProofDeliveryAddress: nil, + }) + + 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) + } + + return sweepVpkt, nil +} From 5bf08b2ece9f9d2c26f9568d63080233b21431c3 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Mon, 14 Jul 2025 23:41:11 +0200 Subject: [PATCH 04/20] swapserverrpc: add proto definitions for server asset deposit RPCs --- swapserverrpc/asset_deposit.pb.go | 1058 ++++++++++++++++++++++++ swapserverrpc/asset_deposit.proto | 151 ++++ swapserverrpc/asset_deposit_grpc.pb.go | 265 ++++++ 3 files changed, 1474 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..94be837aa --- /dev/null +++ b/swapserverrpc/asset_deposit.pb.go @@ -0,0 +1,1058 @@ +// 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 +} + +// InitAssetDepositSigningSessionReq is the request to the Server to initialize +// a signing session for one or more deposits. The Server will return a list of +// nonces that the client must use to sign the deposits. +type InitAssetDepositSigningSessionReq struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // deposit_ids is a list of deposit IDs to sign. + DepositIds []string `protobuf:"bytes,1,rep,name=deposit_ids,json=depositIds,proto3" json:"deposit_ids,omitempty"` +} + +func (x *InitAssetDepositSigningSessionReq) Reset() { + *x = InitAssetDepositSigningSessionReq{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitAssetDepositSigningSessionReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitAssetDepositSigningSessionReq) ProtoMessage() {} + +func (x *InitAssetDepositSigningSessionReq) 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 InitAssetDepositSigningSessionReq.ProtoReflect.Descriptor instead. +func (*InitAssetDepositSigningSessionReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{5} +} + +func (x *InitAssetDepositSigningSessionReq) GetDepositIds() []string { + if x != nil { + return x.DepositIds + } + return nil +} + +// InitDepositSigningSessionRes contains a list of nonces that the client must +// use to sign the deposits. +type InitAssetDepositSigningSessionRes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // nonces is a map which maps deposit_id to nonce. The client can use these + // nonces at most once to sign the deposits. The server may revoke nonces + // in which case the client needs to re-initiate the signing session. + Nonces map[string][]byte `protobuf:"bytes,1,rep,name=nonces,proto3" json:"nonces,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *InitAssetDepositSigningSessionRes) Reset() { + *x = InitAssetDepositSigningSessionRes{} + if protoimpl.UnsafeEnabled { + mi := &file_asset_deposit_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InitAssetDepositSigningSessionRes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InitAssetDepositSigningSessionRes) ProtoMessage() {} + +func (x *InitAssetDepositSigningSessionRes) 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 InitAssetDepositSigningSessionRes.ProtoReflect.Descriptor instead. +func (*InitAssetDepositSigningSessionRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{6} +} + +func (x *InitAssetDepositSigningSessionRes) GetNonces() map[string][]byte { + if x != nil { + return x.Nonces + } + 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 + + // hash is the hash used in the HTLC script. + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + // csv_expiry is the CSV expiry for the HTLC. + CsvExpiry uint32 `protobuf:"varint,2,opt,name=csv_expiry,json=csvExpiry,proto3" json:"csv_expiry,omitempty"` + // 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,3,rep,name=partial_sigs,json=partialSigs,proto3" json:"partial_sigs,omitempty"` + // htlc_psbt is the HTLC psbt. + HtlcPsbt []byte `protobuf:"bytes,4,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[7] + 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[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 PushAssetDepositHtlcSigsReq.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigsReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{7} +} + +func (x *PushAssetDepositHtlcSigsReq) GetHash() []byte { + if x != nil { + return x.Hash + } + return nil +} + +func (x *PushAssetDepositHtlcSigsReq) GetCsvExpiry() uint32 { + if x != nil { + return x.CsvExpiry + } + return 0 +} + +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[8] + 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[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 PushAssetDepositHtlcSigsRes.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigsRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{8} +} + +// 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[9] + 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[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 PushAssetDepositKeysReq.ProtoReflect.Descriptor instead. +func (*PushAssetDepositKeysReq) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{9} +} + +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[10] + 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[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 PushAssetDepositKeysRes.ProtoReflect.Descriptor instead. +func (*PushAssetDepositKeysRes) Descriptor() ([]byte, []int) { + return file_asset_deposit_proto_rawDescGZIP(), []int{10} +} + +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, + 0x44, 0x0a, 0x21, 0x49, 0x6e, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, + 0x6e, 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, 0xae, 0x01, 0x0a, 0x21, 0x49, 0x6e, 0x69, 0x74, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x12, 0x4e, 0x0a, 0x06, 0x6e, + 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x6c, 0x6f, + 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x2e, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x06, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4e, + 0x6f, 0x6e, 0x63, 0x65, 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, 0xb1, 0x01, 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, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, + 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, + 0x63, 0x73, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x42, 0x0a, 0x0c, 0x70, 0x61, 0x72, + 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x73, 0x18, 0x03, 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, 0x04, 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, 0x97, 0x04, 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, 0x78, 0x0a, 0x1e, 0x49, 0x6e, 0x69, 0x74, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, + 0x6e, 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, + 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x2a, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, + 0x49, 0x6e, 0x69, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 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, 14) +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 + (*InitAssetDepositSigningSessionReq)(nil), // 6: looprpc.InitAssetDepositSigningSessionReq + (*InitAssetDepositSigningSessionRes)(nil), // 7: looprpc.InitAssetDepositSigningSessionRes + (*PushAssetDepositHtlcSigsReq)(nil), // 8: looprpc.PushAssetDepositHtlcSigsReq + (*PushAssetDepositHtlcSigsRes)(nil), // 9: looprpc.PushAssetDepositHtlcSigsRes + (*PushAssetDepositKeysReq)(nil), // 10: looprpc.PushAssetDepositKeysReq + (*PushAssetDepositKeysRes)(nil), // 11: looprpc.PushAssetDepositKeysRes + nil, // 12: looprpc.WithdrawAssetDepositsServerRes.DepositKeysEntry + nil, // 13: looprpc.InitAssetDepositSigningSessionRes.NoncesEntry + nil, // 14: looprpc.PushAssetDepositKeysReq.DepositKeysEntry +} +var file_asset_deposit_proto_depIdxs = []int32{ + 12, // 0: looprpc.WithdrawAssetDepositsServerRes.deposit_keys:type_name -> looprpc.WithdrawAssetDepositsServerRes.DepositKeysEntry + 13, // 1: looprpc.InitAssetDepositSigningSessionRes.nonces:type_name -> looprpc.InitAssetDepositSigningSessionRes.NoncesEntry + 5, // 2: looprpc.PushAssetDepositHtlcSigsReq.partial_sigs:type_name -> looprpc.AssetDepositPartialSig + 14, // 3: looprpc.PushAssetDepositKeysReq.deposit_keys:type_name -> looprpc.PushAssetDepositKeysReq.DepositKeysEntry + 1, // 4: looprpc.AssetDepositService.NewAssetDeposit:input_type -> looprpc.NewAssetDepositServerReq + 3, // 5: looprpc.AssetDepositService.WithdrawAssetDeposits:input_type -> looprpc.WithdrawAssetDepositsServerReq + 6, // 6: looprpc.AssetDepositService.InitAssetDepositSigningSession:input_type -> looprpc.InitAssetDepositSigningSessionReq + 8, // 7: looprpc.AssetDepositService.PushAssetDepositHtlcSigs:input_type -> looprpc.PushAssetDepositHtlcSigsReq + 10, // 8: looprpc.AssetDepositService.PushAssetDepositKeys:input_type -> looprpc.PushAssetDepositKeysReq + 2, // 9: looprpc.AssetDepositService.NewAssetDeposit:output_type -> looprpc.NewAssetDepositServerRes + 4, // 10: looprpc.AssetDepositService.WithdrawAssetDeposits:output_type -> looprpc.WithdrawAssetDepositsServerRes + 7, // 11: looprpc.AssetDepositService.InitAssetDepositSigningSession:output_type -> looprpc.InitAssetDepositSigningSessionRes + 9, // 12: looprpc.AssetDepositService.PushAssetDepositHtlcSigs:output_type -> looprpc.PushAssetDepositHtlcSigsRes + 11, // 13: looprpc.AssetDepositService.PushAssetDepositKeys:output_type -> looprpc.PushAssetDepositKeysRes + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] 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.(*InitAssetDepositSigningSessionReq); 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.(*InitAssetDepositSigningSessionRes); 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.(*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[8].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[9].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[10].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: 14, + 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..ca125fb68 --- /dev/null +++ b/swapserverrpc/asset_deposit.proto @@ -0,0 +1,151 @@ +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); + + // InitAssetDepositSigningSession initializes a signing session for one or + // more deposits. The Server will return a list of nonces that the client + // may use at most once to sign the deposits. The server may revoke nonces, + // in which case the client needs to re-initiate the signing session. + rpc InitAssetDepositSigningSession (InitAssetDepositSigningSessionReq) + returns (InitAssetDepositSigningSessionRes); + + // 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; +} + +// InitAssetDepositSigningSessionReq is the request to the Server to initialize +// a signing session for one or more deposits. The Server will return a list of +// nonces that the client must use to sign the deposits. +message InitAssetDepositSigningSessionReq { + // deposit_ids is a list of deposit IDs to sign. + repeated string deposit_ids = 1; +} + +// InitDepositSigningSessionRes contains a list of nonces that the client must +// use to sign the deposits. +message InitAssetDepositSigningSessionRes { + // nonces is a map which maps deposit_id to nonce. The client can use these + // nonces at most once to sign the deposits. The server may revoke nonces + // in which case the client needs to re-initiate the signing session. + map nonces = 1; +} + +// PushAssetDepositHtlcSigsReq holds partial signatures spending one or more +// deposits and the zero fee HTLC spending them. +message PushAssetDepositHtlcSigsReq { + // hash is the hash used in the HTLC script. + bytes hash = 1; + + // csv_expiry is the CSV expiry for the HTLC. + uint32 csv_expiry = 2; + + // 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 = 3; + + // htlc_psbt is the HTLC psbt. + bytes htlc_psbt = 4; +} + +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..5a29a4086 --- /dev/null +++ b/swapserverrpc/asset_deposit_grpc.pb.go @@ -0,0 +1,265 @@ +// 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) + // InitAssetDepositSigningSession initializes a signing session for one or + // more deposits. The Server will return a list of nonces that the client + // may use at most once to sign the deposits. The server may revoke nonces, + // in which case the client needs to re-initiate the signing session. + InitAssetDepositSigningSession(ctx context.Context, in *InitAssetDepositSigningSessionReq, opts ...grpc.CallOption) (*InitAssetDepositSigningSessionRes, 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) InitAssetDepositSigningSession(ctx context.Context, in *InitAssetDepositSigningSessionReq, opts ...grpc.CallOption) (*InitAssetDepositSigningSessionRes, error) { + out := new(InitAssetDepositSigningSessionRes) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositService/InitAssetDepositSigningSession", 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) + // InitAssetDepositSigningSession initializes a signing session for one or + // more deposits. The Server will return a list of nonces that the client + // may use at most once to sign the deposits. The server may revoke nonces, + // in which case the client needs to re-initiate the signing session. + InitAssetDepositSigningSession(context.Context, *InitAssetDepositSigningSessionReq) (*InitAssetDepositSigningSessionRes, 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) InitAssetDepositSigningSession(context.Context, *InitAssetDepositSigningSessionReq) (*InitAssetDepositSigningSessionRes, error) { + return nil, status.Errorf(codes.Unimplemented, "method InitAssetDepositSigningSession 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_InitAssetDepositSigningSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InitAssetDepositSigningSessionReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositServiceServer).InitAssetDepositSigningSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositService/InitAssetDepositSigningSession", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositServiceServer).InitAssetDepositSigningSession(ctx, req.(*InitAssetDepositSigningSessionReq)) + } + 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: "InitAssetDepositSigningSession", + Handler: _AssetDepositService_InitAssetDepositSigningSession_Handler, + }, + { + MethodName: "PushAssetDepositHtlcSigs", + Handler: _AssetDepositService_PushAssetDepositHtlcSigs_Handler, + }, + { + MethodName: "PushAssetDepositKeys", + Handler: _AssetDepositService_PushAssetDepositKeys_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "asset_deposit.proto", +} From b033f71892e12040968ddb6cedc8f180cc5e77a3 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:53:09 +0200 Subject: [PATCH 05/20] 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..351d50feb --- /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/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: amount, + SwapHash: swapHash, + CsvExpiry: csvExpiry, + AddressParams: d.chainParams, + } + + 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 423bb984e3f9757e1ebac6fbb13a11d855940d18 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Sun, 17 Aug 2025 16:37:20 +0200 Subject: [PATCH 06/20] build[tmp]: add replaces needed to support SubmitPackage --- go.mod | 51 +++++++++++++++++++-------- go.sum | 107 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 92 insertions(+), 66 deletions(-) diff --git a/go.mod b/go.mod index 2ea7436a8..68309b6ab 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 - github.com/jackc/pgx/v5 v5.6.0 + github.com/jackc/pgx/v5 v5.7.4 github.com/jessevdk/go-flags v1.4.0 github.com/lib/pq v1.10.9 github.com/lightninglabs/aperture v0.3.13-beta @@ -36,7 +36,7 @@ require ( 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 @@ -73,8 +73,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 @@ -102,11 +102,11 @@ 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/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/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,7 +120,7 @@ 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 @@ -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 +// 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-temp) +replace github.com/lightninglabs/taproot-assets => github.com/bhandras/taproot-assets v0.0.0-20250817135045-e6c8a3e9bfa7 + +// 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-20250814120557-c9b71df55dd9 + go 1.23.12 + +toolchain go1.24.5 diff --git a/go.sum b/go.sum index 0e7c29591..aeea05084 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-20250817135045-e6c8a3e9bfa7 h1:V20EdDW6s+pkciheyyemD3saY/2dWRInCIJeOQ1k4tw= +github.com/bhandras/taproot-assets v0.0.0-20250817135045-e6c8a3e9bfa7/go.mod h1:FcVAHtAZmFjxc2Aos8eyuj5Tl7tWIwSYYW3dkNAsHRQ= +github.com/bhandras/taproot-assets/taprpc v0.0.0-20250814120557-c9b71df55dd9 h1:Gf5PrC4Rm0fskNutfqQ2YMk0Ocg/NpMf9ga/rCYhzxo= +github.com/bhandras/taproot-assets/taprpc v0.0.0-20250814120557-c9b71df55dd9/go.mod h1:tQx1OrBzhtGPJfBVHSWjHH9x4RPv8YOPIdnxzc7H6IU= 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-0.20250729190616-3f323918a96e h1:wlaM8dTlpCQ0uNj0TBskBDeNTTDessxiXiakYDB4RFo= -github.com/lightninglabs/taproot-assets v0.6.1-0.20250729190616-3f323918a96e/go.mod h1:mIgx0p/GkMZeEjEm91vYQsH41YQBAgJl7TP6JcT+wgs= -github.com/lightninglabs/taproot-assets/taprpc v1.0.10-0.20250729190616-3f323918a96e h1:MnXspinwkd6VhV8G9I+TdSak05UitfYyNBCQhTIIr0g= -github.com/lightninglabs/taproot-assets/taprpc v1.0.10-0.20250729190616-3f323918a96e/go.mod h1:c8gTEcKEUoUPVChgZNwqTL1hss7UWa5FDeObr8WBzQk= -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,8 +1132,6 @@ 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.10 h1:ZLV7TGwjnKupVfCd+DJ43MAc9BKVSFCnvhpSPGKdN3M= -github.com/lightningnetwork/lnd/sqldb v1.0.10/go.mod h1:c/vWoQfcxu6FAfHzGajkIQi7CEIeIZFhhH4DYh1BJpc= 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.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= @@ -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= From 8ed1697749d2d8a792fbc19476233e7902378ee6 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 09:21:02 +0200 Subject: [PATCH 07/20] looprpc: add client side asset deposit service proto stubs --- looprpc/client_asset_deposit.pb.go | 961 ++++++++++++++++++++++++ looprpc/client_asset_deposit.proto | 100 +++ looprpc/client_asset_deposit_grpc.pb.go | 245 ++++++ looprpc/perms.go | 20 + 4 files changed, 1326 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..6b0ad3db3 --- /dev/null +++ b/looprpc/client_asset_deposit.pb.go @@ -0,0 +1,961 @@ +// 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"` + SweepScriptKey string `protobuf:"bytes,10,opt,name=sweep_script_key,json=sweepScriptKey,proto3" json:"sweep_script_key,omitempty"` + SweepInternalKey string `protobuf:"bytes,11,opt,name=sweep_internal_key,json=sweepInternalKey,proto3" json:"sweep_internal_key,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) GetSweepScriptKey() string { + if x != nil { + return x.SweepScriptKey + } + return "" +} + +func (x *AssetDeposit) GetSweepInternalKey() string { + if x != nil { + return x.SweepInternalKey + } + 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 PushAssetDepositHtlcSigRequest 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"` + PreimageHash []byte `protobuf:"bytes,2,opt,name=preimage_hash,json=preimageHash,proto3" json:"preimage_hash,omitempty"` + CsvExpiry uint32 `protobuf:"varint,3,opt,name=csv_expiry,json=csvExpiry,proto3" json:"csv_expiry,omitempty"` + Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"` +} + +func (x *PushAssetDepositHtlcSigRequest) Reset() { + *x = PushAssetDepositHtlcSigRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositHtlcSigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositHtlcSigRequest) ProtoMessage() {} + +func (x *PushAssetDepositHtlcSigRequest) 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 PushAssetDepositHtlcSigRequest.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigRequest) Descriptor() ([]byte, []int) { + return file_client_asset_deposit_proto_rawDescGZIP(), []int{9} +} + +func (x *PushAssetDepositHtlcSigRequest) GetDepositId() string { + if x != nil { + return x.DepositId + } + return "" +} + +func (x *PushAssetDepositHtlcSigRequest) GetPreimageHash() []byte { + if x != nil { + return x.PreimageHash + } + return nil +} + +func (x *PushAssetDepositHtlcSigRequest) GetCsvExpiry() uint32 { + if x != nil { + return x.CsvExpiry + } + return 0 +} + +func (x *PushAssetDepositHtlcSigRequest) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +type PushAssetDepositHtlcSigResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *PushAssetDepositHtlcSigResponse) Reset() { + *x = PushAssetDepositHtlcSigResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_client_asset_deposit_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PushAssetDepositHtlcSigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PushAssetDepositHtlcSigResponse) ProtoMessage() {} + +func (x *PushAssetDepositHtlcSigResponse) 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 PushAssetDepositHtlcSigResponse.ProtoReflect.Descriptor instead. +func (*PushAssetDepositHtlcSigResponse) 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, 0x82, 0x03, 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, + 0x28, 0x0a, 0x10, 0x73, 0x77, 0x65, 0x65, 0x70, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x77, 0x65, 0x65, 0x70, + 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x77, 0x65, + 0x65, 0x70, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x73, 0x77, 0x65, 0x65, 0x70, 0x49, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x4b, 0x65, 0x79, 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, 0x99, 0x01, 0x0a, 0x1e, 0x50, 0x75, + 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x74, + 0x6c, 0x63, 0x53, 0x69, 0x67, 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, 0x12, 0x23, 0x0a, 0x0d, 0x70, + 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, + 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x73, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x63, 0x73, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x21, 0x0a, 0x1f, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x44, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x84, 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, 0x6c, 0x0a, 0x17, 0x50, 0x75, 0x73, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x44, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x69, 0x67, 0x12, 0x27, 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, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 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, 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 + (*PushAssetDepositHtlcSigRequest)(nil), // 9: looprpc.PushAssetDepositHtlcSigRequest + (*PushAssetDepositHtlcSigResponse)(nil), // 10: looprpc.PushAssetDepositHtlcSigResponse +} +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.PushAssetDepositHtlcSig:input_type -> looprpc.PushAssetDepositHtlcSigRequest + 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.PushAssetDepositHtlcSig:output_type -> looprpc.PushAssetDepositHtlcSigResponse + 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.(*PushAssetDepositHtlcSigRequest); 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.(*PushAssetDepositHtlcSigResponse); 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..2173fafb1 --- /dev/null +++ b/looprpc/client_asset_deposit.proto @@ -0,0 +1,100 @@ +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 PushAssetDepositHtlcSig (PushAssetDepositHtlcSigRequest) + returns (PushAssetDepositHtlcSigResponse); +} + +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_script_key = 10; + + string sweep_internal_key = 11; +} + +message WithdrawAssetDepositsRequest { + repeated string deposit_ids = 1; +} + +message WithdrawAssetDepositsResponse { +} + +message RevealAssetDepositKeyRequest { + string deposit_id = 1; +} + +message RevealAssetDepositKeyResponse { +} + +message PushAssetDepositHtlcSigRequest { + string deposit_id = 1; + + bytes preimage_hash = 2; + + uint32 csv_expiry = 3; + + bytes nonce = 4; +} + +message PushAssetDepositHtlcSigResponse { +} diff --git a/looprpc/client_asset_deposit_grpc.pb.go b/looprpc/client_asset_deposit_grpc.pb.go new file mode 100644 index 000000000..d4fd2443b --- /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) + PushAssetDepositHtlcSig(ctx context.Context, in *PushAssetDepositHtlcSigRequest, opts ...grpc.CallOption) (*PushAssetDepositHtlcSigResponse, 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) PushAssetDepositHtlcSig(ctx context.Context, in *PushAssetDepositHtlcSigRequest, opts ...grpc.CallOption) (*PushAssetDepositHtlcSigResponse, error) { + out := new(PushAssetDepositHtlcSigResponse) + err := c.cc.Invoke(ctx, "/looprpc.AssetDepositClient/PushAssetDepositHtlcSig", 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) + PushAssetDepositHtlcSig(context.Context, *PushAssetDepositHtlcSigRequest) (*PushAssetDepositHtlcSigResponse, 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) PushAssetDepositHtlcSig(context.Context, *PushAssetDepositHtlcSigRequest) (*PushAssetDepositHtlcSigResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PushAssetDepositHtlcSig 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_PushAssetDepositHtlcSig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PushAssetDepositHtlcSigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AssetDepositClientServer).PushAssetDepositHtlcSig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.AssetDepositClient/PushAssetDepositHtlcSig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AssetDepositClientServer).PushAssetDepositHtlcSig(ctx, req.(*PushAssetDepositHtlcSigRequest)) + } + 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: "PushAssetDepositHtlcSig", + Handler: _AssetDepositClient_PushAssetDepositHtlcSig_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 2d26575cbde4f07a7bccec27d37da0835487ba4c Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:43:51 +0200 Subject: [PATCH 08/20] 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 | 54 ++++++++++++ cmd/loop/asset_deposits.go | 171 +++++++++++++++++++++++++++++++++++++ cmd/loop/main.go | 24 +++++- loopd/daemon.go | 11 +++ loopd/log.go | 4 + loopd/swapclient_server.go | 2 + 7 files changed, 291 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..6f5dbf22e --- /dev/null +++ b/assets/deposit/server.go @@ -0,0 +1,54 @@ +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") +} diff --git a/cmd/loop/asset_deposits.go b/cmd/loop/asset_deposits.go new file mode 100644 index 000000000..3edc03e80 --- /dev/null +++ b/cmd/loop/asset_deposits.go @@ -0,0 +1,171 @@ +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, + }, + } + + 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.", + }, + }, + } +) + +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 +} 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 c5f1dc70b..e9b8a94c7 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() @@ -700,6 +706,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, @@ -717,6 +727,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 8322425bbe2dca90aa6e3995458ebfac16d0c3bf Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:46:58 +0200 Subject: [PATCH 09/20] loopd: add profiler to the Loop daemon --- loopd/daemon.go | 14 +++++++ loopd/profiler.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 loopd/profiler.go diff --git a/loopd/daemon.go b/loopd/daemon.go index e9b8a94c7..ac7785afe 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/http" + _ "net/http/pprof" //nolint:gosec "strings" "sync" "sync/atomic" @@ -101,6 +102,8 @@ type Daemon struct { restCtxCancel func() macaroonService *lndclient.MacaroonService + + profiler *Profiler } // New creates a new instance of the loop client daemon. @@ -133,6 +136,10 @@ func (d *Daemon) Start() error { return errOnlyStartOnce } + // TODO(bhandras): only start if enabled. Make port configurable. + d.profiler = NewProfiler(4321) + d.profiler.Start() + network := lndclient.Network(d.cfg.Network) var err error @@ -1081,6 +1088,13 @@ func (d *Daemon) stop() { d.clientCleanup() } + if d.profiler != nil { + err := d.profiler.Stop() + if err != nil { + errorf("Error stopping profiler: %v", err) + } + } + // Everything should be shutting down now, wait for completion. d.wg.Wait() } diff --git a/loopd/profiler.go b/loopd/profiler.go new file mode 100644 index 000000000..cb0edfa3d --- /dev/null +++ b/loopd/profiler.go @@ -0,0 +1,102 @@ +package loopd + +import ( + "context" + "fmt" + "net/http" + "net/http/pprof" + "sync" + "time" +) + +const ( + profilerCloseTimeout = 5 * time.Second +) + +// Profiler is a wrapper around Go's net/http/pprof package which enables +// graceful startup and shutdown of the pprof server. +type Profiler struct { + srv *http.Server + done chan struct{} + once sync.Once +} + +// NewProfiler creates a new Profiler instance that listens on the specified +// port. +func NewProfiler(port int) *Profiler { + mux := http.NewServeMux() + + // Landing redirect + mux.Handle("/", http.RedirectHandler( + "/debug/pprof/", http.StatusSeeOther), + ) + + // pprof endpoints + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: mux, + // Use some defailt timeouts to prevent hanging connections + // (gosec). + ReadHeaderTimeout: 5 * time.Second, + IdleTimeout: 60 * time.Second, + } + + return &Profiler{ + srv: srv, + done: make(chan struct{}), + } +} + +// Start initializes the pprof server and starts listening for requests. +func (p *Profiler) Start() { + p.once.Do(func() { + go func() { + infof("Profiler (pprof) listening on %s", p.srv.Addr) + err := p.srv.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + errorf("Profiler (pprof) server error: %v", + err) + } + + // Signal termination. + close(p.done) + }() + }) +} + +// Stop attempts a graceful shutdown and waits for completion. Pass a context +// with timeout to limit how long you’re willing to wait. +func (p *Profiler) Stop() error { + ctxt, cancel := context.WithTimeout( + context.Background(), profilerCloseTimeout, + ) + defer cancel() + + // Ask the server to gracefully shut down: stop accepting new + // connections, let in-flight handlers finish. + err := p.srv.Shutdown(ctxt) + if err != nil { + // Attempt to force close the server if shutdown fails. + err = p.srv.Close() + if err != nil { + errorf("Profiler (pprof) server close error: %v", + err) + } + } + + // Wait for the ListenAndServe goroutine to exit. + select { + case <-p.done: + case <-ctxt.Done(): + // If the caller’s context ends before goroutine exit, bail out. + return ctxt.Err() + } + + return nil +} From 61483e3f4e3629dbbd6607e876d8cdea86ee9841 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 10:56:19 +0200 Subject: [PATCH 10/20] 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 | 348 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 348 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..3cca2e782 --- /dev/null +++ b/assets/deposit/sweeper.go @@ -0,0 +1,348 @@ +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/asset" + "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 an interactive deposit sweep using the +// MuSig2 keyspend path. +func (s *Sweeper) PublishDepositSweepMuSig2(ctx context.Context, deposit *Kit, + funder bool, depositProof *proof.Proof, + otherInternalKey *btcec.PrivateKey, sweepScriptKey asset.ScriptKey, + sweepInternalKey *btcec.PublicKey, label string, + 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 + } + + // Create the sweep vpacket which is simply sweeping the asset on the + // OP_TRUE output to a new output with the provided script and internal + // keys. + sweepVpkt, err := assets.CreateOpTrueSweepVpkt( + ctx, []*proof.Proof{depositProof}, sweepScriptKey, + sweepInternalKey, nil, &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, + ) + if err != nil { + return nil, err + } + + 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, label, + ) + + 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, sweepScriptKey asset.ScriptKey, + sweepInternalKey *btcec.PublicKey, label string, + 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 a new output with the provided script and internal + // keys. + sweepVpkt, err := assets.CreateOpTrueSweepVpkt( + ctx, []*proof.Proof{depositProof}, sweepScriptKey, + sweepInternalKey, nil, &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, label, + ) + 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 29833e343585d10131d56c4bc216ac05ea017c12 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:12:12 +0200 Subject: [PATCH 11/20] loopdb: add basic schema for asset deposits --- .../migrations/000018_asset_deposits.down.sql | 1 + .../migrations/000018_asset_deposits.up.sql | 101 ++++++++++++++++++ loopdb/sqlc/models.go | 35 ++++++ 3 files changed, 137 insertions(+) create mode 100644 loopdb/sqlc/migrations/000018_asset_deposits.down.sql create mode 100644 loopdb/sqlc/migrations/000018_asset_deposits.up.sql diff --git a/loopdb/sqlc/migrations/000018_asset_deposits.down.sql b/loopdb/sqlc/migrations/000018_asset_deposits.down.sql new file mode 100644 index 000000000..3ac57554b --- /dev/null +++ b/loopdb/sqlc/migrations/000018_asset_deposits.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS asset_deposits; diff --git a/loopdb/sqlc/migrations/000018_asset_deposits.up.sql b/loopdb/sqlc/migrations/000018_asset_deposits.up.sql new file mode 100644 index 000000000..a02f734b3 --- /dev/null +++ b/loopdb/sqlc/migrations/000018_asset_deposits.up.sql @@ -0,0 +1,101 @@ +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_script_pubkey is the script key that will be used for the asset + -- after the deposit is swept. + sweep_script_pubkey BLOB, + + -- sweep_internal_pubkey is the internal public key that will be used + -- for the asset output after the deposit is swept. + sweep_internal_pubkey BLOB +); + +-- 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 46766aca1..82a265aa9 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -9,6 +9,41 @@ 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 + SweepScriptPubkey []byte + SweepInternalPubkey []byte +} + +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 6c6a3a4b0ca46545dbb2e483a92294081a3f3026 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:42:25 +0200 Subject: [PATCH 12/20] 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 | 248 ++++++++++++++++++++++++++ assets/deposit/manager.go | 312 +++++++++++++++++++++++++++++++++ assets/deposit/manager_test.go | 176 +++++++++++++++++++ assets/deposit/protocol.go | 44 +++++ assets/deposit/server.go | 8 +- assets/deposit/sql_store.go | 53 ++++++ assets/deposit/store.go | 10 ++ loopd/daemon.go | 43 ++++- loopd/swapclient_server.go | 2 +- 9 files changed, 891 insertions(+), 5 deletions(-) create mode 100644 assets/deposit/deposit.go create mode 100644 assets/deposit/manager.go create mode 100644 assets/deposit/manager_test.go create mode 100644 assets/deposit/protocol.go create mode 100644 assets/deposit/sql_store.go create mode 100644 assets/deposit/store.go diff --git a/assets/deposit/deposit.go b/assets/deposit/deposit.go new file mode 100644 index 000000000..12cd0229d --- /dev/null +++ b/assets/deposit/deposit.go @@ -0,0 +1,248 @@ +package deposit + +import ( + "context" + "encoding/hex" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/lightninglabs/loop/assets" + "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 +} + +// Info holds publicly available information about an asset deposit. +// It is used to communicate deposit details to clients of the deposit Manager. +type Info 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 + + // Version 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 + + // SweepScriptKey is the script key of the swept asset. + SweepScriptKey *btcec.PublicKey + + // SweepInternalKey is the internal key of output of the swept asset. + SweepInternalKey *btcec.PublicKey +} + +// Copy creates a copy of the Info struct. +func (d *Info) Copy() *Info { + info := &Info{ + ID: d.ID, + Version: d.Version, + CreatedAt: d.CreatedAt, + Amount: d.Amount, + Addr: d.Addr, + State: d.State, + ConfirmationHeight: d.ConfirmationHeight, + Expiry: d.Expiry, + } + + if d.Outpoint != nil { + info.Outpoint = &wire.OutPoint{ + Hash: d.Outpoint.Hash, + Index: d.Outpoint.Index, + } + } + + if d.SweepScriptKey != nil { + pubKey := *d.SweepScriptKey + info.SweepScriptKey = &pubKey + } + + if d.SweepInternalKey != nil { + pubKey := *d.SweepInternalKey + info.SweepInternalKey = &pubKey + } + + return info +} + +// Deposit is the struct that holds all the information about an asset deposit. +type Deposit struct { + *Kit + + *Info + + // 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 +} + +// fundingLabel 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) fundingLabel() string { + return fmt.Sprintf("deposit funding %v", d.ID) +} + +// timeoutSweepLabel is a string label that we can use for marking a timeout +// sweep transfer. This is useful if we need to filter deposits. +func (d *Deposit) timeoutSweepLabel() string { + return fmt.Sprintf("deposit timeout sweep %v", d.ID) +} + +// withdrawLabel is a string label that we can use for marking a withdrawal +// sweep transfer. This is useful if we need to filter deposits. +func (d *Deposit) withdrawLabel() string { + return fmt.Sprintf("deposit withdraw sweep %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 +} + +// GenerateSweepKeys generates the sweep script key and internal key for the +// deposit sweep. +func (d *Deposit) GenerateSweepKeys(ctx context.Context, + client *assets.TapdClient) error { + + if d.SweepScriptKey != nil { + return nil + } + + scriptKey, internalKeyDesc, err := client.DeriveNewKeys(ctx) + if err != nil { + return err + } + + d.SweepScriptKey = scriptKey.PubKey + d.SweepInternalKey = internalKeyDesc.PubKey + + return nil +} diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go new file mode 100644 index 000000000..bdaf639ad --- /dev/null +++ b/assets/deposit/manager.go @@ -0,0 +1,312 @@ +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(*Info) + +// 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 store. + store Store + + // 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 outer map key is the deposit ID, the inner map key is a unique + // subscriber ID. + subscribers map[string]map[uint64]DepositUpdateCallback + + // nextSubscriberID is the next available ID for a subscriber. + nextSubscriberID uint64 + + // 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 Store, + 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]map[uint64]DepositUpdateCallback), //nolint:lll + 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) (func(), error) { + + done, err := m.scheduleNextCall() + if err != nil { + return func() {}, err + } + defer done() + + d, ok := m.deposits[depositID] + if !ok { + return func() {}, fmt.Errorf("deposit %s not found", depositID) + } + + log.Infof("Registering deposit state update subscriber for deposit: %s", + d.ID) + + // Get the next available subscriber ID. + subscriberID := m.nextSubscriberID + m.nextSubscriberID++ + + // If this is the first subscriber for this deposit, we need to create + // the inner map. + if _, ok := m.subscribers[d.ID]; !ok { + m.subscribers[d.ID] = make(map[uint64]DepositUpdateCallback) + } + + // Store the subscriber. + m.subscribers[d.ID][subscriberID] = subscriber + + // Send the current deposit info to the subscriber right away in a + // non-blocking way. + go subscriber(d.Info.Copy()) + + // Return a function that can be used to unsubscribe. + unsubscribe := func() { + done, err := m.scheduleNextCall() + if err != nil { + log.Errorf("Error unsubscribing from deposit "+ + "updates: %v", err) + return + } + defer done() + + log.Infof("Unsubscribing from deposit state updates for "+ + "deposit: %s, subscriber: %d", d.ID, subscriberID) + + // If the subscriber map for the deposit exists, delete the + // subscriber from it. + if depositSubscribers, ok := m.subscribers[d.ID]; ok { + delete(depositSubscribers, subscriberID) + + // If there are no more subscribers for this deposit, + // delete the deposit's entry from the subscribers map. + if len(depositSubscribers) == 0 { + delete(m.subscribers, d.ID) + } + } + } + + return unsubscribe, 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.Info.Copy()) + } + + return nil +} diff --git a/assets/deposit/manager_test.go b/assets/deposit/manager_test.go new file mode 100644 index 000000000..ff771385e --- /dev/null +++ b/assets/deposit/manager_test.go @@ -0,0 +1,176 @@ +package deposit + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/test" + "github.com/stretchr/testify/require" +) + +// mockStore is a mock implementation of the Store interface. +type mockStore struct { + updated bool +} + +// AddAssetDeposit is a mock implementation of the AddAssetDeposit method. +func (s *mockStore) AddAssetDeposit(context.Context, *Deposit) error { + return nil +} + +// AddDepositUpdate marks the store as updated. +func (s *mockStore) AddDepositUpdate(context.Context, *Info) error { + s.updated = true + return nil +} + +// UpdateDeposit is a mock implementation of the UpdateDeposit method. +func (s *mockStore) UpdateDeposit(context.Context, *Deposit) error { + s.updated = true + return nil +} + +// testAddDeposit is a helper function that (intrusively) adds a deposit to the +// manager. +func testAddDeposit(t *testing.T, m *Manager, d *Deposit) { + t.Helper() + + done, err := m.scheduleNextCall() + require.NoError(t, err) + defer done() + + m.deposits[d.Info.ID] = d +} + +// testUpdateDeposit updates a deposit's state in a race-safe way by using the +// manager's internal synchronization. +func testUpdateDeposit(t *testing.T, m *Manager, d *Deposit, state State, + outpoint *wire.OutPoint) { + + t.Helper() + + done, err := m.scheduleNextCall() + require.NoError(t, err) + defer done() + + d.State = state + if outpoint != nil { + d.Outpoint = outpoint + } + + err = m.handleDepositStateUpdate(m.runCtx(), d) + require.NoError(t, err) +} + +// TestSubscribeDepositUpdates tests the subscription and unsubscription of +// deposit updates. +func TestSubscribeDepositUpdates(t *testing.T) { + // The test guard ensures that there are no goroutines left running + // after the test completes. + defer test.Guard(t)() + + const updateTimeout = 500 * time.Millisecond + + // Create a mock lnd instance to be able to use the chain notifier. + lnd := test.NewMockLnd() + store := &mockStore{} + + // Create a new manager with a mock clock and chain notifier. + m := NewManager( + nil, nil, nil, lnd.ChainNotifier, nil, store, + &chaincfg.SimNetParams, + ) + + // We'll use this context for our calls. + ctxb := context.Background() + + // Start the manager run loop. + ctxc, cancel := context.WithCancel(ctxb) + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + + err := m.Run(ctxc, 0) + require.NoError(t, err) + }() + + // Create a dummy deposit and add it to the manager. + depositID := "test_deposit" + testDeposit := &Deposit{ + Info: &Info{ + ID: depositID, + State: StatePending, + }, + } + testAddDeposit(t, m, testDeposit) + + // Create a channel to receive deposit updates. + updateChan := make(chan *Info) + + // Define the update callback. + updateCallback := func(info *Info) { + updateChan <- info + } + + // assertUpdate is a helper function that waits for a deposit update and + // asserts its state. + assertUpdate := func(expectedState State) { + t.Helper() + + select { + case update := <-updateChan: + require.Equal(t, depositID, update.ID) + require.Equal(t, expectedState, update.State) + + case <-time.After(updateTimeout): + t.Fatalf("expected to receive deposit update with "+ + "state %v", expectedState) + } + } + + // Subscribe to deposit updates. + unsubscribe, err := m.SubscribeDepositUpdates( + depositID, updateCallback, + ) + require.NoError(t, err) + require.NotNil(t, unsubscribe) + + // The subscriber should receive the current deposit info right away. + assertUpdate(StatePending) + + // Trigger a deposit state update using the helper function. + testUpdateDeposit(t, m, testDeposit, StateConfirmed, &wire.OutPoint{ + Hash: chainhash.Hash{1, 2, 3}, + Index: 0, + }) + + // The subscriber should receive the update. + assertUpdate(StateConfirmed) + + // Now, unsubscribe. + unsubscribe() + + // Trigger another deposit state update. + testUpdateDeposit(t, m, testDeposit, StateSwept, nil) + + // The subscriber should not receive any more updates. + select { + case <-updateChan: + t.Fatal("expected not to receive deposit update after " + + "unsubscribing") + + case <-time.After(updateTimeout): + // All good, no update received. + } + + // Stop the manager and wait until it finishes. + cancel() + wg.Wait() +} diff --git a/assets/deposit/protocol.go b/assets/deposit/protocol.go new file mode 100644 index 000000000..61b32416e --- /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 <= 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 6f5dbf22e..5b55d30b2 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..20dd297aa --- /dev/null +++ b/assets/deposit/sql_store.go @@ -0,0 +1,53 @@ +package deposit + +import ( + "context" + + "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/assets/deposit/store.go b/assets/deposit/store.go new file mode 100644 index 000000000..fb2d54692 --- /dev/null +++ b/assets/deposit/store.go @@ -0,0 +1,10 @@ +package deposit + +import "context" + +// Store defines the interface that the Manager requires from the storage layer. +type Store interface { + // UpdateDeposit updates the deposit state and extends the deposit + // update log in the SQL store. + UpdateDeposit(ctx context.Context, d *Deposit) error +} diff --git a/loopd/daemon.go b/loopd/daemon.go index ac7785afe..ffe68483d 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -430,6 +430,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() @@ -507,6 +513,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()) @@ -588,6 +598,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. @@ -711,11 +722,24 @@ 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{ @@ -734,8 +758,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. @@ -1002,6 +1026,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 685d50cb7ea24f7486f32fd4623d66cd0e196c14 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 13:59:41 +0200 Subject: [PATCH 13/20] 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 | 393 +++++++++++++++++++++++++ assets/deposit/server.go | 49 ++- assets/deposit/sql_store.go | 83 +++++- assets/deposit/store.go | 3 + loopdb/sqlc/asset_deposits.sql.go | 110 +++++++ loopdb/sqlc/querier.go | 3 + loopdb/sqlc/queries/asset_deposits.sql | 29 ++ 7 files changed, 667 insertions(+), 3 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 bdaf639ad..9f94e4059 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -4,15 +4,29 @@ import ( "context" "errors" "fmt" + "strings" + "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" + "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/loop/swapserverrpc" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rpcutils" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" ) 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 " + @@ -310,3 +324,382 @@ 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) (Info, error) { + + clientKeyDesc, err := m.walletKit.DeriveNextKey( + ctx, AssetDepositKeyFamily, + ) + if err != nil { + return Info{}, err + } + clientInternalPubKey, _, err := DeriveSharedDepositKey( + ctx, m.signer, clientKeyDesc.PubKey, + ) + if err != nil { + return Info{}, 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 Info{}, err + } + + serverScriptPubKey, err := btcec.ParsePubKey(resp.ServerScriptPubkey) + if err != nil { + return Info{}, err + } + + serverInternalPubKey, err := btcec.ParsePubKey( + resp.ServerInternalPubkey, + ) + if err != nil { + return Info{}, err + } + + kit, err := NewKit( + clientKeyDesc.PubKey, clientInternalPubKey, serverScriptPubKey, + serverInternalPubKey, clientKeyDesc.KeyLocator, assetID, + csvExpiry, &m.addressParams, + ) + if err != nil { + return Info{}, err + } + + deposit := &Deposit{ + Kit: kit, + Info: &Info{ + 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 Info{}, err + } + + err = m.handleNewDeposit(ctx, deposit) + if err != nil { + log.Errorf("Unable to add deposit to active deposits: %v", err) + + return Info{}, err + } + + return *deposit.Info.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.fundingLabel(), + }, + ) + 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 +} + +// 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. Note that if the proof is + // already imoported then this will be a no-op and just return the + // last proof (ie the deposit proof). + 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 +} + +// registerDepositAsKnown registers the deposit with the tapd server so it +// can claim the funds committed to the OP_TRUE script. +func (m *Manager) registerDepositAsKnown(ctx context.Context, + d *Deposit) error { + + opTrueScriptKey, _, _, _, err := htlc.CreateOpTrueLeaf() + if err != nil { + return err + } + + // Declare the script key as known on the underlying tapd. Note that + // under the hood this is an upsert operation, so if the script is + // already known it will be a no-op. This is useful since we use the + // same OP_TRUE script key for all deposits. + _, err = m.tapClient.DeclareScriptKey( + ctx, &assetwalletrpc.DeclareScriptKeyRequest{ + ScriptKey: rpcutils.MarshalScriptKey( + opTrueScriptKey, + ), + }, + ) + if err != nil { + return err + } + + // Now let the underlying tapd know about the deposit transfer so it can + // then claim the funds committed to the OP_TRUE script. + opTrueScriptKeyBytes := opTrueScriptKey.PubKey.SerializeCompressed() + opTrueScriptKeyBytes[0] = secp256k1.PubKeyFormatCompressedEven + + _, err = m.tapClient.RegisterTransfer( + ctx, &taprpc.RegisterTransferRequest{ + AssetId: d.AssetID[:], + ScriptKey: opTrueScriptKeyBytes, + Outpoint: &taprpc.OutPoint{ + Txid: d.Outpoint.Hash[:], + OutputIndex: d.Outpoint.Index, + }, + }, + ) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + log.Debugf("Deposit %v already registered as known: %v", + d.ID, err) + + return nil + } + + return err + } + + return nil +} + +// waitForDepositConfirmation waits for the deposit to be confirmed. +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.fundingLabel(), + ) + 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 +} + +// 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 { + log.Errorf("Unable to cache proof info for deposit %v: %v", + d.ID, err) + + return err + } + + err = m.registerDepositAsKnown(ctx, d) + if err != nil { + log.Errorf("Unable to register deposit %v as known: %v", + d.ID, err) + + 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 5b55d30b2..7e34803c6 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 length: 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 20dd297aa..108576d63 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -2,9 +2,11 @@ package deposit import ( "context" + "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" ) @@ -12,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 @@ -46,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/assets/deposit/store.go b/assets/deposit/store.go index fb2d54692..1fa9a5078 100644 --- a/assets/deposit/store.go +++ b/assets/deposit/store.go @@ -4,6 +4,9 @@ import "context" // Store defines the interface that the Manager requires from the storage layer. type Store interface { + // AddAssetDeposit adds a new asset deposit to the database. + AddAssetDeposit(ctx context.Context, d *Deposit) error + // UpdateDeposit updates the deposit state and extends the deposit // update log in the SQL store. UpdateDeposit(ctx context.Context, d *Deposit) error 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 0a380e426..73d0953ad 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 @@ -65,10 +66,12 @@ type Querier interface { InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error IsStored(ctx context.Context, swapHash []byte) (bool, error) MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error + MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, 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 b954e59d1b76e02512e31476313a15d2cd1da3e4 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:06:24 +0200 Subject: [PATCH 14/20] 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/manager_test.go | 5 + assets/deposit/server.go | 50 ++++++++- assets/deposit/sql_store.go | 137 +++++++++++++++++++++++++ assets/deposit/store.go | 3 + loopdb/sqlc/asset_deposits.sql.go | 82 +++++++++++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 12 +++ 8 files changed, 326 insertions(+), 1 deletion(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 9f94e4059..df4645347 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -703,3 +703,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/manager_test.go b/assets/deposit/manager_test.go index ff771385e..93bb32867 100644 --- a/assets/deposit/manager_test.go +++ b/assets/deposit/manager_test.go @@ -35,6 +35,11 @@ func (s *mockStore) UpdateDeposit(context.Context, *Deposit) error { return nil } +// GetAllDeposits is a mock implementation of the GetAllDeposits method. +func (s *mockStore) GetAllDeposits(context.Context) ([]Deposit, error) { + return []Deposit{}, nil +} + // testAddDeposit is a helper function that (intrusively) adds a deposit to the // manager. func testAddDeposit(t *testing.T, m *Manager, d *Deposit) { diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 7e34803c6..a8d7fe371 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -83,7 +83,55 @@ 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, + } + + if d.Outpoint != nil { + rpcDeposit.AnchorOutpoint = d.Outpoint.String() + } + + if d.SweepScriptKey != nil { + rpcDeposit.SweepScriptKey = hex.EncodeToString( + d.SweepScriptKey.SerializeCompressed(), + ) + } + + if d.SweepInternalKey != nil { + rpcDeposit.SweepInternalKey = hex.EncodeToString( + d.SweepInternalKey.SerializeCompressed(), + ) + } + + 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..36435d6f1 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,132 @@ func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error { ) }) } + +// GetAllDeposits returns all deposits known to the store. +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 := &Info{ + ID: sqlDeposit.DepositID, + Version: AssetDepositProtocolVersion( + sqlDeposit.ProtocolVersion, + ), + CreatedAt: sqlDeposit.CreatedAt.Local(), //nolint:gosmopolitan + 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 len(sqlDeposit.SweepInternalPubkey) > 0 { + sweepInternalPubKey, err := btcec.ParsePubKey( + sqlDeposit.SweepInternalPubkey, + ) + if err != nil { + return Deposit{}, err + } + depositInfo.SweepInternalKey = sweepInternalPubKey + } + + if len(sqlDeposit.SweepScriptPubkey) > 0 { + sweepScriptPubKey, err := btcec.ParsePubKey( + sqlDeposit.SweepScriptPubkey, + ) + if err != nil { + return Deposit{}, err + } + depositInfo.SweepScriptKey = sweepScriptPubKey + } + + 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, + Info: depositInfo, + }, nil +} diff --git a/assets/deposit/store.go b/assets/deposit/store.go index 1fa9a5078..8f2a8dd90 100644 --- a/assets/deposit/store.go +++ b/assets/deposit/store.go @@ -10,4 +10,7 @@ type Store interface { // UpdateDeposit updates the deposit state and extends the deposit // update log in the SQL store. UpdateDeposit(ctx context.Context, d *Deposit) error + + // GetAllDeposits returns all deposits known to the store. + GetAllDeposits(ctx context.Context) ([]Deposit, error) } diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 9de22a865..5f1825611 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 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_script_pubkey, d.sweep_internal_pubkey, 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 + SweepScriptPubkey []byte + SweepInternalPubkey []byte + 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.SweepScriptPubkey, + &i.SweepInternalPubkey, + &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 73d0953ad..2d4554b56 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -24,6 +24,7 @@ type Querier interface { DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, 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 183d542e91671fbe07435c57c79d706a6f5ef806 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:35:58 +0200 Subject: [PATCH 15/20] 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 | 17 ++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 + 5 files changed, 296 insertions(+) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index df4645347..a2450aae4 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -20,6 +20,7 @@ import ( "github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + "google.golang.org/protobuf/proto" ) var ( @@ -31,6 +32,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 @@ -67,6 +72,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 @@ -118,6 +127,7 @@ func NewManager(depositServiceClient swapserverrpc.AssetDepositServiceClient, sweeper: sweeper, addressParams: addressParams, deposits: make(map[string]*Deposit), + pendingSweeps: make(map[string]struct{}), subscribers: make(map[string]map[uint64]DepositUpdateCallback), //nolint:lll callEnter: make(chan struct{}), callLeave: make(chan struct{}), @@ -220,6 +230,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 } @@ -740,3 +787,212 @@ 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 { + d.State = StateExpired + err := d.GenerateSweepKeys(ctx, m.tapClient) + if err != nil { + log.Errorf("Unable to generate sweep keys for deposit %v: %v", + d.ID, err) + } + + 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 { + 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, + ) + if err != nil { + return err + } + + lockID, err := d.lockID() + if err != nil { + return err + } + + snedResp, err := m.sweeper.PublishDepositTimeoutSweep( + ctx, d.Kit, d.Proof, asset.NewScriptKey(d.SweepScriptKey), + d.SweepInternalKey, d.timeoutSweepLabel(), + feeRateSatPerKw.FeePerVByte(), lockID, lockExpiration, + ) + if err != nil { + // TODO(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 + } + } + } + + // Start monitoring the sweep unless we're already doing so. + if _, ok := m.pendingSweeps[d.ID]; !ok { + err := m.waitForDepositSweep(ctx, d, d.timeoutSweepLabel()) + if err != nil { + log.Errorf("Unable to wait for deposit %v spend: %v", + d.ID, err) + + return err + } + + m.pendingSweeps[d.ID] = struct{}{} + } + + 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) waitForDepositSweep(ctx context.Context, d *Deposit, + label string) error { + + log.Infof("Waiting for deposit sweep confirmation %s", d.ID) + + eventChan, errChan, err := m.tapClient.WaitForSendComplete( + ctx, d.SweepScriptKey.SerializeCompressed(), label, + ) + if err != nil { + log.Errorf("unable to subscribe to send events for deposit "+ + "sweep: %v", err, + ) + } + + go func() { + select { + case event := <-eventChan: + // At this point we can consider the deposit confirmed. + err = m.handleDepositSpend(ctx, d, event.Transfer) + if err != nil { + m.criticalError(err) + } + + case err := <-errChan: + m.criticalError(err) + } + }() + + return nil +} + +func formatProtoJSON(resp proto.Message) (string, error) { + jsonBytes, err := taprpc.ProtoJSONMarshalOpts.Marshal(resp) + if err != nil { + return "", err + } + + return string(jsonBytes), nil +} + +func toJSON(resp proto.Message) string { + jsonStr, _ := formatProtoJSON(resp) + + return jsonStr +} + +// 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, + transfer *taprpc.AssetTransfer) 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: + 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 + } + + log.Tracef("Deposit %s spent in transfer: %s\n", d.ID, toJSON(transfer)) + + // TODO(bhandras): should save the spend details to the store? + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + 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 36435d6f1..f647bb8e6 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) + + SetAssetDepositSweepKeys(ctx context.Context, + arg sqlc.SetAssetDepositSweepKeysParams) 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: + scriptKey := d.SweepScriptKey.SerializeCompressed() + internalKey := d.SweepInternalKey.SerializeCompressed() + err := tx.SetAssetDepositSweepKeys( + ctx, sqlc.SetAssetDepositSweepKeysParams{ + DepositID: d.ID, + SweepScriptPubkey: scriptKey, + SweepInternalPubkey: internalKey, + }, + ) + 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 5f1825611..d47029eda 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -172,6 +172,23 @@ func (q *Queries) MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfi return err } +const setAssetDepositSweepKeys = `-- name: SetAssetDepositSweepKeys :exec +UPDATE asset_deposits +SET sweep_script_pubkey = $2, sweep_internal_pubkey = $3 +WHERE deposit_id = $1 +` + +type SetAssetDepositSweepKeysParams struct { + DepositID string + SweepScriptPubkey []byte + SweepInternalPubkey []byte +} + +func (q *Queries) SetAssetDepositSweepKeys(ctx context.Context, arg SetAssetDepositSweepKeysParams) error { + _, err := q.db.ExecContext(ctx, setAssetDepositSweepKeys, arg.DepositID, arg.SweepScriptPubkey, arg.SweepInternalPubkey) + 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 2d4554b56..ac9b1046b 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -69,6 +69,7 @@ type Querier interface { MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + SetAssetDepositSweepKeys(ctx context.Context, arg SetAssetDepositSweepKeysParams) error SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, 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 304acee2e..5c9e11655 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: SetAssetDepositSweepKeys :exec +UPDATE asset_deposits +SET sweep_script_pubkey = $2, sweep_internal_pubkey = $3 +WHERE deposit_id = $1; From f0ed1b6a86dfe6ecd330211af8a4a8102567ac2e Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:41:33 +0200 Subject: [PATCH 16/20] 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 | 57 +++++++++++++++++ assets/deposit/manager_test.go | 5 ++ assets/deposit/sql_store.go | 27 +++++++++ assets/deposit/store.go | 4 ++ loopdb/sqlc/asset_deposits.sql.go | 84 ++++++++++++++++++++++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 15 +++++ 7 files changed, 193 insertions(+) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index a2450aae4..53a0bf355 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -156,6 +156,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, ) @@ -228,6 +235,56 @@ 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 + } + + // Register the deposit as known so that we can claim + // the funds committed to the OP_TRUE script. If the + // deposit is already registered, this will be a no-op. + err = m.registerDepositAsKnown(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/manager_test.go b/assets/deposit/manager_test.go index 93bb32867..781e26d2c 100644 --- a/assets/deposit/manager_test.go +++ b/assets/deposit/manager_test.go @@ -40,6 +40,11 @@ func (s *mockStore) GetAllDeposits(context.Context) ([]Deposit, error) { return []Deposit{}, nil } +// GetActiveDeposits is a mock implementation of the GetActiveDeposits method. +func (s *mockStore) GetActiveDeposits(context.Context) ([]Deposit, error) { + return []Deposit{}, nil +} + // testAddDeposit is a helper function that (intrusively) adds a deposit to the // manager. func testAddDeposit(t *testing.T, m *Manager, d *Deposit) { diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index f647bb8e6..01b005050 100644 --- a/assets/deposit/sql_store.go +++ b/assets/deposit/sql_store.go @@ -32,6 +32,9 @@ type Querier interface { SetAssetDepositSweepKeys(ctx context.Context, arg sqlc.SetAssetDepositSweepKeysParams) error + + GetActiveAssetDeposits(ctx context.Context) ( + []sqlc.GetActiveAssetDepositsRow, error) } // DepositBaseDB is the interface that contains all the queries generated @@ -284,3 +287,27 @@ func sqlcDepositToDeposit(sqlDeposit sqlc.GetAssetDepositsRow, Info: 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/assets/deposit/store.go b/assets/deposit/store.go index 8f2a8dd90..6932ca4ff 100644 --- a/assets/deposit/store.go +++ b/assets/deposit/store.go @@ -13,4 +13,8 @@ type Store interface { // GetAllDeposits returns all deposits known to the store. GetAllDeposits(ctx context.Context) ([]Deposit, error) + + // GetActiveDeposits returns all active deposits from the database. + // Active deposits are those that have not yet been spent or swept. + GetActiveDeposits(ctx context.Context) ([]Deposit, error) } diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index d47029eda..bda60511f 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -67,6 +67,90 @@ 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_script_pubkey, d.sweep_internal_pubkey, 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 + SweepScriptPubkey []byte + SweepInternalPubkey []byte + 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.SweepScriptPubkey, + &i.SweepInternalPubkey, + &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_script_pubkey, d.sweep_internal_pubkey, u.update_state, u.update_timestamp FROM asset_deposits d diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index ac9b1046b..515f3a583 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -23,6 +23,7 @@ type Querier interface { DepositIDsForSwapHash(ctx context.Context, swapHash []byte) ([][]byte, error) DepositsForSwapHash(ctx context.Context, swapHash []byte) ([]DepositsForSwapHashRow, 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 5c9e11655..ee5e9ea56 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_script_pubkey = $2, sweep_internal_pubkey = $3 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 17ba66fbcd8c308fd7a15d742b14abe06e86fe9d Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 14:48:05 +0200 Subject: [PATCH 17/20] 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 | 73 ++++++++++++++++++++++++++ assets/deposit/manager_test.go | 9 ++++ assets/deposit/server.go | 16 +++++- assets/deposit/sql_store.go | 18 +++++++ assets/deposit/store.go | 11 +++- loopdb/sqlc/asset_deposits.sql.go | 17 ++++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 ++ 8 files changed, 148 insertions(+), 2 deletions(-) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 53a0bf355..bf687bde5 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -1053,3 +1053,76 @@ 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 + } + + d.State = StateWithdrawn + err = d.GenerateSweepKeys(ctx, m.tapClient) + if err != nil { + log.Errorf("Unable to generate sweep keys for deposit "+ + "withdrawal %v: %v", d.ID, err) + + return err + } + + err = m.handleDepositStateUpdate(ctx, d) + if err != nil { + return err + } + } + + return nil +} diff --git a/assets/deposit/manager_test.go b/assets/deposit/manager_test.go index 781e26d2c..5eab1652c 100644 --- a/assets/deposit/manager_test.go +++ b/assets/deposit/manager_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -45,6 +46,14 @@ func (s *mockStore) GetActiveDeposits(context.Context) ([]Deposit, error) { return []Deposit{}, nil } +// SetAssetDepositServerKey is a mock implementation of the +// SetAssetDepositServerKey method. +func (s *mockStore) SetAssetDepositServerKey(context.Context, string, + *btcec.PrivateKey) error { + + return nil +} + // testAddDeposit is a helper function that (intrusively) adds a deposit to the // manager. func testAddDeposit(t *testing.T, m *Manager, d *Deposit) { diff --git a/assets/deposit/server.go b/assets/deposit/server.go index a8d7fe371..eecdfbc25 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -149,5 +149,19 @@ 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 } diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index 01b005050..61f800bef 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: scriptKey := d.SweepScriptKey.SerializeCompressed() internalKey := d.SweepInternalKey.SerializeCompressed() err := tx.SetAssetDepositSweepKeys( @@ -311,3 +316,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/assets/deposit/store.go b/assets/deposit/store.go index 6932ca4ff..0598a4862 100644 --- a/assets/deposit/store.go +++ b/assets/deposit/store.go @@ -1,6 +1,10 @@ package deposit -import "context" +import ( + "context" + + "github.com/btcsuite/btcd/btcec/v2" +) // Store defines the interface that the Manager requires from the storage layer. type Store interface { @@ -17,4 +21,9 @@ type Store interface { // GetActiveDeposits returns all active deposits from the database. // Active deposits are those that have not yet been spent or swept. GetActiveDeposits(ctx context.Context) ([]Deposit, error) + + // SetAssetDepositServerKey sets the server's internal key for the given + // asset deposit. + SetAssetDepositServerKey(ctx context.Context, depositID string, + key *btcec.PrivateKey) error } diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index bda60511f..2b7afde57 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -256,6 +256,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 setAssetDepositSweepKeys = `-- name: SetAssetDepositSweepKeys :exec UPDATE asset_deposits SET sweep_script_pubkey = $2, sweep_internal_pubkey = $3 diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 515f3a583..b68f4459f 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -70,6 +70,7 @@ type Querier interface { MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error MarkDepositConfirmed(ctx context.Context, arg MarkDepositConfirmedParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + SetAssetDepositServerInternalKey(ctx context.Context, arg SetAssetDepositServerInternalKeyParams) error SetAssetDepositSweepKeys(ctx context.Context, arg SetAssetDepositSweepKeysParams) error SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error diff --git a/loopdb/sqlc/queries/asset_deposits.sql b/loopdb/sqlc/queries/asset_deposits.sql index ee5e9ea56..1a31805fe 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 00469c8e12698a430165b083bd2e5964b99e0b35 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Sun, 17 Aug 2025 16:38:42 +0200 Subject: [PATCH 18/20] 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 | 86 ++++++++++++++++++++++++++ assets/deposit/manager_test.go | 10 +++ assets/deposit/sql_store.go | 19 ++++++ assets/deposit/store.go | 5 ++ loopdb/sqlc/asset_deposits.sql.go | 13 ++++ loopdb/sqlc/querier.go | 1 + loopdb/sqlc/queries/asset_deposits.sql | 5 ++ 7 files changed, 139 insertions(+) diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index bf687bde5..957058df7 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -173,6 +173,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: @@ -192,6 +197,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) @@ -985,6 +999,8 @@ func (m *Manager) handleDepositSpend(ctx context.Context, d *Deposit, switch d.State { case StateTimeoutSweepPublished: + fallthrough + case StateCooperativeSweepPublished: d.State = StateSwept err := m.releaseDepositSweepInputs(ctx, d) @@ -1126,3 +1142,73 @@ 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 + } + + serverKey, err := m.store.GetAssetDepositServerKey( + ctx, d.ID, + ) + if err != nil { + return err + } + + lockID, err := d.lockID() + if err != nil { + return err + } + + // TODO(bhandras): conf target should be dynamic/configrable. + const confTarget = 2 + feeRateSatPerKw, err := m.walletKit.EstimateFeeRate( + ctx, confTarget, + ) + if err != nil { + return err + } + + funder := true + sendResp, err := m.sweeper.PublishDepositSweepMuSig2( + ctx, d.Kit, funder, d.Proof, serverKey, + asset.NewScriptKey(d.SweepScriptKey), + d.SweepInternalKey, d.withdrawLabel(), + 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, + sendResp.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 + } + } + + // Start monitoring the sweep unless we're already doing so. + if _, ok := m.pendingSweeps[d.ID]; !ok { + err := m.waitForDepositSweep(ctx, d, d.withdrawLabel()) + if err != nil { + log.Errorf("Unable to wait for deposit %v "+ + "spend: %v", d.ID, err) + + return err + } + + m.pendingSweeps[d.ID] = struct{}{} + } + } + + return nil +} diff --git a/assets/deposit/manager_test.go b/assets/deposit/manager_test.go index 5eab1652c..7ae86c211 100644 --- a/assets/deposit/manager_test.go +++ b/assets/deposit/manager_test.go @@ -54,6 +54,16 @@ func (s *mockStore) SetAssetDepositServerKey(context.Context, string, return nil } +// GetAssetDepositServerKey is a mock implementation of the +// GetAssetDepositServerKey method. +func (s *mockStore) GetAssetDepositServerKey(context.Context, string) ( + *btcec.PrivateKey, error) { + + // Return a dummy private key for testing + privKey, _ := btcec.NewPrivateKey() + return privKey, nil +} + // testAddDeposit is a helper function that (intrusively) adds a deposit to the // manager. func testAddDeposit(t *testing.T, m *Manager, d *Deposit) { diff --git a/assets/deposit/sql_store.go b/assets/deposit/sql_store.go index 61f800bef..7f1f81218 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 @@ -329,3 +332,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/assets/deposit/store.go b/assets/deposit/store.go index 0598a4862..c530f6e22 100644 --- a/assets/deposit/store.go +++ b/assets/deposit/store.go @@ -26,4 +26,9 @@ type Store interface { // asset deposit. SetAssetDepositServerKey(ctx context.Context, depositID string, key *btcec.PrivateKey) error + + // GetAssetDepositServerKey gets the server's internal key for the given + // asset deposit. + GetAssetDepositServerKey(ctx context.Context, depositID string) ( + *btcec.PrivateKey, error) } diff --git a/loopdb/sqlc/asset_deposits.sql.go b/loopdb/sqlc/asset_deposits.sql.go index 2b7afde57..ec52ea976 100644 --- a/loopdb/sqlc/asset_deposits.sql.go +++ b/loopdb/sqlc/asset_deposits.sql.go @@ -151,6 +151,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_script_pubkey, d.sweep_internal_pubkey, u.update_state, u.update_timestamp FROM asset_deposits d diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index b68f4459f..5d2132062 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -25,6 +25,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 1a31805fe..5e3a99141 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 ed52492940ce7248c82335bee156deffa5d63e87 Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 15:00:10 +0200 Subject: [PATCH 19/20] 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 957058df7..06430feab 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -1212,3 +1212,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 eecdfbc25..91262c4e0 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -140,7 +140,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 6a02fe04b8d74c98a525394017a959850e4233bb Mon Sep 17 00:00:00 2001 From: Andras Banki-Horvath Date: Tue, 15 Jul 2025 15:29:25 +0200 Subject: [PATCH 20/20] 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 | 87 +++++++++++++++++++++++++++++ assets/deposit/server.go | 38 +++++++++++++ assets/deposit/signer.go | 112 ++++++++++++++++++++++++++++++++++++++ assets/deposit/sweeper.go | 69 +++++++++++++++++++++++ looprpc/perms.go | 4 ++ 5 files changed, 310 insertions(+) create mode 100644 assets/deposit/signer.go diff --git a/assets/deposit/manager.go b/assets/deposit/manager.go index 06430feab..eab68baca 100644 --- a/assets/deposit/manager.go +++ b/assets/deposit/manager.go @@ -1,6 +1,7 @@ package deposit import ( + "bytes" "context" "errors" "fmt" @@ -8,8 +9,10 @@ import ( "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/decred/dcrd/dcrec/secp256k1/v4" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/assets" @@ -20,6 +23,7 @@ import ( "github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + "github.com/lightningnetwork/lnd/lntypes" "google.golang.org/protobuf/proto" ) @@ -1285,3 +1289,86 @@ func (m *Manager) RevealDepositKeys(ctx context.Context, return err } + +// PushHtlcSig will partially a deposit spending zero-fee HTLC and send the +// resulting signature to the swap server. +func (m *Manager) PushHtlcSig(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 + } + + funder := true + depositSigner := NewMuSig2Signer( + m.signer, deposit.Kit, funder, deposit.AnchorRootHash, + ) + + err = depositSigner.NewSession(ctx) + if err != nil { + return fmt.Errorf("Unable to create MuSig2 session: %w", err) + } + + publicNonce, err := depositSigner.PubNonce() + if err != nil { + return err + } + + partialSig, err := depositSigner.PartialSignMuSig2( + serverNonce, sigHash, + ) + if err != nil { + log.Errorf("Unable to create partial deposit signature %v: %v", + deposit.ID, err) + + return err + } + + var pktBuf bytes.Buffer + err = htlcPkt.Serialize(&pktBuf) + if err != nil { + return err + } + + // TODO(bhandras): the server should return the final signature. + _, err = m.depositServiceClient.PushAssetDepositHtlcSigs( + ctx, &swapserverrpc.PushAssetDepositHtlcSigsReq{ + Hash: hash[:], + CsvExpiry: csvExpiry, + PartialSigs: []*swapserverrpc.AssetDepositPartialSig{ + { + DepositId: depositID, + Nonce: publicNonce[:], + PartialSig: partialSig, + }, + }, + HtlcPsbt: pktBuf.Bytes(), + }, + ) + + return err +} diff --git a/assets/deposit/server.go b/assets/deposit/server.go index 91262c4e0..7879a4646 100644 --- a/assets/deposit/server.go +++ b/assets/deposit/server.go @@ -5,8 +5,10 @@ import ( "encoding/hex" "fmt" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "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" ) @@ -176,3 +178,39 @@ func (s *Server) WithdrawAssetDeposits(ctx context.Context, return &looprpc.WithdrawAssetDepositsResponse{}, nil } + +// PushAssetDepositHtlcSig is the rpc endpoint for loop clients to push partial +// signatures for asset deposit spending zero fee HTLCs to the server. +func (s *Server) PushAssetDepositHtlcSig(ctx context.Context, + in *looprpc.PushAssetDepositHtlcSigRequest) ( + *looprpc.PushAssetDepositHtlcSigResponse, error) { + + if len(in.Nonce) != musig2.PubNonceSize { + return nil, status.Error(codes.InvalidArgument, + fmt.Sprintf("invalid nonce length: expected %d bytes, "+ + "got %d", musig2.PubNonceSize, len(in.Nonce))) + } + + if len(in.PreimageHash) != lntypes.HashSize { + return nil, status.Error(codes.InvalidArgument, + fmt.Sprintf("invalid preimage hash length: expected "+ + "%d bytes, got %d", lntypes.HashSize, + len(in.PreimageHash))) + } + + var ( + nonce [musig2.PubNonceSize]byte + preimageHash lntypes.Hash + ) + copy(nonce[:], in.Nonce) + copy(preimageHash[:], in.PreimageHash) + + err := s.manager.PushHtlcSig( + ctx, in.DepositId, nonce, preimageHash, in.CsvExpiry, + ) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &looprpc.PushAssetDepositHtlcSigResponse{}, nil +} diff --git a/assets/deposit/signer.go b/assets/deposit/signer.go new file mode 100644 index 000000000..b670da5ae --- /dev/null +++ b/assets/deposit/signer.go @@ -0,0 +1,112 @@ +package deposit + +import ( + "bytes" + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/input" +) + +type MuSig2Signer struct { + signer lndclient.SignerClient + + scriptKey *btcec.PublicKey + otherPubKey *btcec.PublicKey + anchorRootHash []byte + + session input.MuSig2Session +} + +func NewMuSig2Signer(signer lndclient.SignerClient, deposit *Kit, funder bool, + anchorRootHash []byte) *MuSig2Signer { + + scriptKey := deposit.FunderScriptKey + otherPubKey := deposit.CoSignerInternalKey + if !funder { + scriptKey = deposit.CoSignerScriptKey + otherPubKey = deposit.FunderInternalKey + } + + return &MuSig2Signer{ + signer: signer, + scriptKey: scriptKey, + otherPubKey: otherPubKey, + anchorRootHash: anchorRootHash, + } +} + +func (s *MuSig2Signer) NewSession(ctx context.Context) error { + // Derive the internal key that will be used to sign the message. + signingPubKey, signingPrivKey, err := DeriveSharedDepositKey( + ctx, s.signer, s.scriptKey, + ) + if err != nil { + return err + } + + pubKeys := []*btcec.PublicKey{ + signingPubKey, s.otherPubKey, + } + + tweaks := &input.MuSig2Tweaks{ + TaprootTweak: s.anchorRootHash, + } + + _, session, err := input.MuSig2CreateContext( + input.MuSig2Version100RC2, signingPrivKey, pubKeys, tweaks, nil, + ) + if err != nil { + return fmt.Errorf("error creating signing context: %w", err) + } + + s.session = session + + return nil +} + +func (s *MuSig2Signer) Session() input.MuSig2Session { + return s.session +} + +func (s *MuSig2Signer) PubNonce() ([musig2.PubNonceSize]byte, error) { + // If we don't have a session, we can't return a public nonce. + if s.session == nil { + return [musig2.PubNonceSize]byte{}, fmt.Errorf("no session " + + "available") + } + + // Return the public nonce of the current session. + return s.session.PublicNonce(), nil +} + +// PartialSignMuSig2 is used to partially sign a message hash with the deposit's +// keys. +func (s *MuSig2Signer) PartialSignMuSig2(otherNonce [musig2.PubNonceSize]byte, + message [32]byte) ([]byte, error) { + + if s.session == nil { + return nil, fmt.Errorf("no session available") + } + + _, err := s.session.RegisterPubNonce(otherNonce) + if err != nil { + return nil, err + } + + partialSig, err := input.MuSig2Sign(s.session, message, true) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = partialSig.Encode(&buf) + if err != nil { + return nil, fmt.Errorf("error encoding partial sig: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/assets/deposit/sweeper.go b/assets/deposit/sweeper.go index 3cca2e782..b39991f91 100644 --- a/assets/deposit/sweeper.go +++ b/assets/deposit/sweeper.go @@ -16,12 +16,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/asset" "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/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) @@ -346,3 +350,68 @@ 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 + } + htlcScriptKey := asset.NewScriptKey(&htlcAddr.ScriptKey) + + // 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}, htlcScriptKey, + &htlcAddr.InternalKey, htlcAddr.TapscriptSibling, + &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 +} diff --git a/looprpc/perms.go b/looprpc/perms.go index 573f5d4b6..4ba5f0cc4 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -196,4 +196,8 @@ var RequiredPermissions = map[string][]bakery.Op{ Entity: "swap", Action: "execute", }}, + "/looprpc.AssetDepositClient/PushAssetDepositHtlcSig": {{ + Entity: "swap", + Action: "execute", + }}, }