diff --git a/assets/client.go b/assets/client.go index 88c23efe5..49765249c 100644 --- a/assets/client.go +++ b/assets/client.go @@ -8,20 +8,43 @@ import ( "sync" "time" + "bytes" + "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" + 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/tappsbt" "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" "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/tapdevrpc" "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/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntypes" + "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" ) @@ -65,7 +88,10 @@ type TapdClient struct { tapchannelrpc.TaprootAssetChannelsClient priceoraclerpc.PriceOracleClient rfqrpc.RfqClient + wrpc.AssetWalletClient + mintrpc.MintClient universerpc.UniverseClient + tapdevrpc.TapDevClient cfg *TapdConfig assetNameCache map[string]string @@ -313,3 +339,499 @@ func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { return conn, 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) ( + *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 + } + + commitRequest := &assetwalletrpc.CommitVirtualPsbtsRequest{ + Fees: &assetwalletrpc.CommitVirtualPsbtsRequest_SatPerVbyte{ + SatPerVbyte: uint64(feeRateSatPerVByte), + }, + AnchorChangeOutput: &assetwalletrpc.CommitVirtualPsbtsRequest_Add{ //nolint:lll + Add: true, + }, + VirtualPsbts: [][]byte{ + encodedVpkt, + }, + } + 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) ( + *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, + } + + 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 +} + +// ListAvailableAssets returns a list of available assets. +func (t *TapdClient) ListAvailableAssets(ctx context.Context) ( + [][]byte, error) { + + balanceRes, err := t.ListBalances(ctx, &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + }) + if err != nil { + return nil, err + } + + assets := make([][]byte, 0, len(balanceRes.AssetBalances)) + for assetID := range balanceRes.AssetBalances { + asset, err := hex.DecodeString(assetID) + if err != nil { + return nil, err + } + assets = append(assets, asset) + } + + return assets, 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 +} + +// 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 + } + + var proofBytes bytes.Buffer + err = lastProof.Encode(&proofBytes) + if err != nil { + return nil, err + } + + asset := lastProof.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 nil, err + } + + outpoint := &universerpc.Outpoint{ + HashStr: lastProof.AnchorTx.TxHash().String(), + Index: int32(lastProof.InclusionProof.OutputIndex), + } + + scriptKey := lastProof.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(), + }, + }) + if err != nil { + return nil, err + } + } + + return lastProof, nil +} + +func (t *TapdClient) AddHoldInvoice(ctx context.Context, pHash lntypes.Hash, + assetId []byte, assetAmt uint64, memo string) ( + *tapchannelrpc.AddInvoiceResponse, error) { + + // Now we can create the swap invoice. + invoiceRes, err := t.AddInvoice( + ctx, &tapchannelrpc.AddInvoiceRequest{ + + // Todo(sputn1ck):if we have more than one peer, we'll need to + // specify one. This will likely be changed on the tapd front in + // the future. + PeerPubkey: nil, + AssetId: assetId, + AssetAmount: assetAmt, + InvoiceRequest: &lnrpc.Invoice{ + Memo: memo, + RHash: pHash[:], + // todo fix expiries + CltvExpiry: 144, + Expiry: 60, + Private: true, + }, + HodlInvoice: &tapchannelrpc.HodlInvoice{ + PaymentHash: pHash[:], + }, + }, + ) + if err != nil { + return nil, err + } + + return invoiceRes, nil +} + +func (t *TapdClient) SendPayment(ctx context.Context, + invoice string, assetId []byte) (chan *tapchannelrpc.SendPaymentResponse, + chan error, error) { + + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice, + } + + sendReq := &tapchannelrpc.SendPaymentRequest{ + AssetId: assetId, + PaymentRequest: req, + } + + sendResp, err := t.TaprootAssetChannelsClient.SendPayment( + ctx, sendReq, + ) + if err != nil { + return nil, nil, err + } + + sendRespChan := make(chan *tapchannelrpc.SendPaymentResponse) + errChan := make(chan error) + go func() { + defer close(sendRespChan) + defer close(errChan) + for { + select { + case <-ctx.Done(): + errChan <- ctx.Err() + return + default: + res, err := sendResp.Recv() + if err != nil { + errChan <- err + return + } + sendRespChan <- res + } + } + }() + + return sendRespChan, errChan, nil +} 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..8d562b80e --- /dev/null +++ b/assets/htlc/swapkit.go @@ -0,0 +1,457 @@ +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. Note that + // we use btcutil.Amount here for simplicity, but the actual amount + // is in the asset's native unit. + 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 +} + +// GetSuccessScript returns the success path script of the swap HTLC. +func (s *SwapKit) GetSuccessScript() ([]byte, error) { + return GenSuccessPathScript(s.ReceiverPubKey, s.SwapHash) +} + +// GetTimeoutScript returns the timeout path script of the swap HTLC. +func (s *SwapKit) GetTimeoutScript() ([]byte, error) { + return GenTimeoutPathScript(s.SenderPubKey, int64(s.CsvExpiry)) +} + +// GetAggregateKey returns the aggregate MuSig2 key used in the swap HTLC. +func (s *SwapKit) GetAggregateKey() (*btcec.PublicKey, error) { + aggregateKey, err := input.MuSig2CombineKeys( + input.MuSig2Version100RC2, + []*btcec.PublicKey{ + s.SenderPubKey, s.ReceiverPubKey, + }, + true, + &input.MuSig2Tweaks{}, + ) + if err != nil { + return nil, err + } + + return aggregateKey.PreTweakedKey, nil +} + +// GetTimeOutLeaf returns the timeout leaf of the swap. +func (s *SwapKit) GetTimeOutLeaf() (txscript.TapLeaf, error) { + timeoutScript, err := s.GetTimeoutScript() + if err != nil { + return txscript.TapLeaf{}, err + } + + timeoutLeaf := txscript.NewBaseTapLeaf(timeoutScript) + + return timeoutLeaf, nil +} + +// GetSuccessLeaf returns the success leaf of the swap. +func (s *SwapKit) GetSuccessLeaf() (txscript.TapLeaf, error) { + successScript, err := s.GetSuccessScript() + if err != nil { + return txscript.TapLeaf{}, err + } + + successLeaf := txscript.NewBaseTapLeaf(successScript) + + return successLeaf, nil +} + +// GetSiblingPreimage returns the sibling preimage of the HTLC bitcoin top level +// output. +func (s *SwapKit) GetSiblingPreimage() (commitment.TapscriptPreimage, error) { + timeOutLeaf, err := s.GetTimeOutLeaf() + if err != nil { + return commitment.TapscriptPreimage{}, err + } + + successLeaf, err := s.GetSuccessLeaf() + if err != nil { + return commitment.TapscriptPreimage{}, err + } + + branch := txscript.NewTapBranch(timeOutLeaf, successLeaf) + + siblingPreimage := commitment.NewPreimageFromBranch(branch) + + return siblingPreimage, nil +} + +// CreateHtlcVpkt creates the vpacket for the HTLC. +func (s *SwapKit) CreateHtlcVpkt(addressParams *address.ChainParams) ( + *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: addressParams, + Version: tappsbt.V1, + } + pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{ + Amount: 0, + Type: tappsbt.TypeSplitRoot, + AnchorOutputIndex: 0, + ScriptKey: asset.NUMSScriptKey, + }) + pkt.Outputs = append(pkt.Outputs, &tappsbt.VOutput{ + // todo(sputn1ck) assetversion + AssetVersion: asset.Version(1), + Amount: s.Amount, + Interactive: true, + AnchorOutputIndex: 1, + ScriptKey: asset.NewScriptKey( + tapScriptKey.PubKey, + ), + AnchorOutputInternalKey: btcInternalKey, + AnchorOutputTapscriptSibling: &siblingPreimage, + }) + + return pkt, nil +} + +// GenTimeoutBtcControlBlock generates the control block for the timeout path of +// the swap. +func (s *SwapKit) GenTimeoutBtcControlBlock(taprootAssetRoot []byte) ( + *txscript.ControlBlock, error) { + + internalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + successLeaf, err := s.GetSuccessLeaf() + if err != nil { + return nil, err + } + + successLeafHash := successLeaf.TapHash() + + btcControlBlock := &txscript.ControlBlock{ + InternalKey: internalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: append( + successLeafHash[:], taprootAssetRoot..., + ), + } + + timeoutPathScript, err := s.GetTimeoutScript() + if err != nil { + return nil, err + } + + rootHash := btcControlBlock.RootHash(timeoutPathScript) + tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash) + if tapKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd { + + btcControlBlock.OutputKeyYIsOdd = true + } + + return btcControlBlock, nil +} + +// GenSuccessBtcControlBlock generates the control block for the timeout path of +// the swap. +func (s *SwapKit) GenSuccessBtcControlBlock(taprootAssetRoot []byte) ( + *txscript.ControlBlock, error) { + + internalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + timeOutLeaf, err := s.GetTimeOutLeaf() + if err != nil { + return nil, err + } + + timeOutLeafHash := timeOutLeaf.TapHash() + + btcControlBlock := &txscript.ControlBlock{ + InternalKey: internalKey, + LeafVersion: txscript.BaseLeafVersion, + InclusionProof: append( + timeOutLeafHash[:], taprootAssetRoot..., + ), + } + + successPathScript, err := s.GetSuccessScript() + if err != nil { + return nil, err + } + + rootHash := btcControlBlock.RootHash(successPathScript) + tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash) + if tapKey.SerializeCompressed()[0] == + secp256k1.PubKeyFormatCompressedOdd { + + btcControlBlock.OutputKeyYIsOdd = true + } + + return btcControlBlock, nil +} + +// GenTaprootAssetRootFromProof generates the taproot asset root from the proof +// of the swap. +func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) { + assetCopy := proof.Asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets( + &version, assetCopy, + ) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot := assetCommitment.TapscriptRoot(nil) + + return taprootAssetRoot[:], nil +} + +// GetPkScriptFromAsset returns the toplevel bitcoin script with the given +// asset. +func (s *SwapKit) GetPkScriptFromAsset(asset *asset.Asset) ([]byte, error) { + assetCopy := asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets( + &version, assetCopy, + ) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + siblingPreimage, err := s.GetSiblingPreimage() + if err != nil { + return nil, err + } + + siblingHash, err := siblingPreimage.TapHash() + if err != nil { + return nil, err + } + + btcInternalKey, err := s.GetAggregateKey() + if err != nil { + return nil, err + } + + return tapscript.PayToAddrScript( + *btcInternalKey, siblingHash, *assetCommitment, + ) +} + +// CreatePreimageWitness creates a preimage witness for the swap. +func (s *SwapKit) CreatePreimageWitness(ctx context.Context, + signer lndclient.SignerClient, htlcProof *proof.Proof, + sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator, + preimage lntypes.Preimage) (wire.TxWitness, error) { + + assetTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value, + } + feeTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value, + } + + sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = 1 + + successScript, err := s.GetSuccessScript() + if err != nil { + return nil, err + } + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: keyLocator, + }, + SignMethod: input.TaprootScriptSpendSignMethod, + WitnessScript: successScript, + Output: assetTxOut, + InputIndex: 0, + } + sig, err := signer.SignOutputRaw( + ctx, sweepBtcPacket.UnsignedTx, + []*lndclient.SignDescriptor{ + signDesc, + }, + []*wire.TxOut{ + assetTxOut, feeTxOut, + }, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof) + if err != nil { + return nil, err + } + + successControlBlock, err := s.GenSuccessBtcControlBlock( + taprootAssetRoot, + ) + if err != nil { + return nil, err + } + + controlBlockBytes, err := successControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + preimage[:], + sig[0], + successScript, + controlBlockBytes, + }, nil +} + +// CreateTimeoutWitness creates a timeout witness for the swap. +func (s *SwapKit) CreateTimeoutWitness(ctx context.Context, + signer lndclient.SignerClient, htlcProof *proof.Proof, + sweepBtcPacket *psbt.Packet, keyLocator keychain.KeyLocator) ( + wire.TxWitness, error) { + + assetTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[0].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[0].WitnessUtxo.Value, + } + feeTxOut := &wire.TxOut{ + PkScript: sweepBtcPacket.Inputs[1].WitnessUtxo.PkScript, + Value: sweepBtcPacket.Inputs[1].WitnessUtxo.Value, + } + + sweepBtcPacket.UnsignedTx.TxIn[0].Sequence = s.CsvExpiry + + timeoutScript, err := s.GetTimeoutScript() + if err != nil { + return nil, err + } + + signDesc := &lndclient.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{ + KeyLocator: keyLocator, + }, + SignMethod: input.TaprootScriptSpendSignMethod, + WitnessScript: timeoutScript, + Output: assetTxOut, + InputIndex: 0, + } + sig, err := signer.SignOutputRaw( + ctx, sweepBtcPacket.UnsignedTx, + []*lndclient.SignDescriptor{ + signDesc, + }, + []*wire.TxOut{ + assetTxOut, feeTxOut, + }, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot, err := GenTaprootAssetRootFromProof(htlcProof) + if err != nil { + return nil, err + } + + timeoutControlBlock, err := s.GenTimeoutBtcControlBlock( + taprootAssetRoot, + ) + if err != nil { + return nil, err + } + + controlBlockBytes, err := timeoutControlBlock.ToBytes() + if err != nil { + return nil, err + } + + return wire.TxWitness{ + sig[0], + timeoutScript, + controlBlockBytes, + }, nil +} diff --git a/assets/interfaces.go b/assets/interfaces.go new file mode 100644 index 000000000..daf90a3c7 --- /dev/null +++ b/assets/interfaces.go @@ -0,0 +1,153 @@ +package assets + +import ( + "context" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" + "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" + wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +const ( + // DefaultSwapCSVExpiry is the default expiry for a swap in blocks. + DefaultSwapCSVExpiry = int32(24) + + defaultHtlcFeeConfTarget = 3 + defaultHtlcConfRequirement = 2 + + AssetKeyFamily = 696969 +) + +// TapdClient is an interface that groups the methods required to interact with +// the taproot-assets server and the wallet. +type AssetClient interface { + taprpc.TaprootAssetsClient + wrpc.AssetWalletClient + mintrpc.MintClient + universerpc.UniverseClient + tapdevrpc.TapDevClient + + // FundAndSignVpacket funds ands signs a vpacket. + FundAndSignVpacket(ctx context.Context, + vpkt *tappsbt.VPacket) (*tappsbt.VPacket, error) + + // PrepareAndCommitVirtualPsbts prepares and commits virtual psbts. + PrepareAndCommitVirtualPsbts(ctx context.Context, + vpkt *tappsbt.VPacket, feeRateSatPerKVByte chainfee.SatPerVByte, + changeKeyDesc *keychain.KeyDescriptor, params *chaincfg.Params) ( + *psbt.Packet, []*tappsbt.VPacket, []*tappsbt.VPacket, + *assetwalletrpc.CommitVirtualPsbtsResponse, error) + + // LogAndPublish logs and publishes the virtual psbts. + LogAndPublish(ctx context.Context, btcPkt *psbt.Packet, + activeAssets []*tappsbt.VPacket, passiveAssets []*tappsbt.VPacket, + commitResp *wrpc.CommitVirtualPsbtsResponse) (*taprpc.SendAssetResponse, + error) + + // GetAssetBalance returns the balance of the given asset. + GetAssetBalance(ctx context.Context, assetId []byte) ( + uint64, error) + + // DeriveNewKeys derives a new internal and script key. + DeriveNewKeys(ctx context.Context) (asset.ScriptKey, + keychain.KeyDescriptor, error) + + // AddHoldInvoice adds a new hold invoice. + AddHoldInvoice(ctx context.Context, pHash lntypes.Hash, + assetId []byte, assetAmt uint64, memo string) ( + *tapchannelrpc.AddInvoiceResponse, error) + + // ImportProofFile imports the proof file and returns the last proof. + ImportProofFile(ctx context.Context, rawProofFile []byte) ( + *proof.Proof, error) + + // SendPayment pays a payment request. + SendPayment(ctx context.Context, + invoice string, assetId []byte) (chan *tapchannelrpc.SendPaymentResponse, + chan error, error) +} + +// SwapStore is an interface that groups the methods required to store swap +// information. +type SwapStore interface { + // CreateAssetSwapOut creates a new swap out in the store. + CreateAssetSwapOut(ctx context.Context, swap *SwapOut) error + + // UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of a swap out. + UpdateAssetSwapHtlcOutpoint(ctx context.Context, swapHash lntypes.Hash, + outpoint *wire.OutPoint, confirmationHeight int32) error + + // UpdateAssetSwapOutProof updates the proof of a swap out. + UpdateAssetSwapOutProof(ctx context.Context, swapHash lntypes.Hash, + rawProof []byte) error + + // UpdateAssetSwapOutSweepTx updates the sweep tx of a swap out. + UpdateAssetSwapOutSweepTx(ctx context.Context, + swapHash lntypes.Hash, sweepTxid chainhash.Hash, + confHeight int32, sweepPkscript []byte) error + + // InsertAssetSwapUpdate inserts a new swap update in the store. + InsertAssetSwapUpdate(ctx context.Context, + swapHash lntypes.Hash, state fsm.StateType) error + + UpdateAssetSwapOutPreimage(ctx context.Context, + swapHash lntypes.Hash, preimage lntypes.Preimage) error +} + +// BlockHeightSubscriber is responsible for subscribing to the expiry height +// of a swap, as well as getting the current block height. +type BlockHeightSubscriber interface { + // SubscribeExpiry subscribes to the expiry of a swap. It returns true + // if the expiry is already past. Otherwise, it returns false and calls + // the expiryFunc when the expiry height is reached. + SubscribeExpiry(swapHash [32]byte, + expiryHeight int32, expiryFunc func()) bool + // GetBlockHeight returns the current block height. + GetBlockHeight() int32 +} + +// InvoiceSubscriber is responsible for subscribing to an invoice. +type InvoiceSubscriber interface { + // SubscribeInvoice subscribes to an invoice. The update callback is + // called when the invoice is updated and the error callback is called + // when an error occurs. + SubscribeInvoice(ctx context.Context, invoiceHash lntypes.Hash, + updateCallback func(lndclient.InvoiceUpdate, error)) error +} + +// TxConfirmationSubscriber is responsible for subscribing to the confirmation +// of a transaction. +type TxConfirmationSubscriber interface { + + // SubscribeTxConfirmation subscribes to the confirmation of a + // pkscript on the chain. The callback is called when the pkscript is + // confirmed or when an error occurs. + SubscribeTxConfirmation(ctx context.Context, swapHash lntypes.Hash, + txid *chainhash.Hash, pkscript []byte, numConfs int32, + eightHint int32, cb func(*chainntnfs.TxConfirmation, error)) error +} + +// ExchangeRateProvider is responsible for providing the exchange rate between +// assets. +type ExchangeRateProvider interface { + // GetSatsPerAssetUnit returns the amount of satoshis per asset unit. + GetSatsPerAssetUnit(assetId []byte) (btcutil.Amount, error) +} diff --git a/assets/log.go b/assets/log.go new file mode 100644 index 000000000..70981c586 --- /dev/null +++ b/assets/log.go @@ -0,0 +1,26 @@ +package assets + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "ASSETS" + +// 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/store.go b/assets/store.go new file mode 100644 index 000000000..8e9e783a6 --- /dev/null +++ b/assets/store.go @@ -0,0 +1,291 @@ +package assets + +import ( + "context" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/assets/htlc" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightninglabs/loop/loopdb/sqlc" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" +) + +// BaseDB is the interface that contains all the queries generated +// by sqlc for the instantout table. +type BaseDB interface { + // ExecTx allows for executing a function in the context of a database + // transaction. + ExecTx(ctx context.Context, txOptions loopdb.TxOptions, + txBody func(*sqlc.Queries) error) error + + CreateAssetSwap(ctx context.Context, arg sqlc.CreateAssetSwapParams) error + CreateAssetOutSwap(ctx context.Context, swapHash []byte) error + GetAllAssetOutSwaps(ctx context.Context) ([]sqlc.GetAllAssetOutSwapsRow, error) + GetAssetOutSwap(ctx context.Context, swapHash []byte) (sqlc.GetAssetOutSwapRow, error) + InsertAssetSwapUpdate(ctx context.Context, arg sqlc.InsertAssetSwapUpdateParams) error + UpdateAssetSwapHtlcTx(ctx context.Context, arg sqlc.UpdateAssetSwapHtlcTxParams) error + UpdateAssetSwapOutPreimage(ctx context.Context, arg sqlc.UpdateAssetSwapOutPreimageParams) error + UpdateAssetSwapOutProof(ctx context.Context, arg sqlc.UpdateAssetSwapOutProofParams) error + UpdateAssetSwapSweepTx(ctx context.Context, arg sqlc.UpdateAssetSwapSweepTxParams) error +} + +// PostgresStore is the backing store for the instant out manager. +type PostgresStore struct { + queries BaseDB + clock clock.Clock +} + +// NewPostgresStore creates a new PostgresStore. +func NewPostgresStore(queries BaseDB) *PostgresStore { + return &PostgresStore{ + queries: queries, + clock: clock.NewDefaultClock(), + } +} + +// CreateAssetSwapOut creates a new asset swap out in the database. +func (p *PostgresStore) CreateAssetSwapOut(ctx context.Context, + swap *SwapOut) error { + + params := sqlc.CreateAssetSwapParams{ + SwapHash: swap.SwapHash[:], + AssetID: swap.AssetID, + Amt: int64(swap.Amount), + SenderPubkey: swap.SenderPubKey.SerializeCompressed(), + ReceiverPubkey: swap.ReceiverPubKey.SerializeCompressed(), + CsvExpiry: int32(swap.CsvExpiry), + InitiationHeight: int32(swap.InitiationHeight), + CreatedTime: p.clock.Now(), + ServerKeyFamily: int64(swap.ClientKeyLocator.Family), + ServerKeyIndex: int64(swap.ClientKeyLocator.Index), + } + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + err := q.CreateAssetSwap(ctx, params) + if err != nil { + return err + } + + return q.CreateAssetOutSwap(ctx, swap.SwapHash[:]) + }, + ) +} + +// UpdateAssetSwapHtlcOutpoint updates the htlc outpoint of the swap out in the +// database. +func (p *PostgresStore) UpdateAssetSwapHtlcOutpoint(ctx context.Context, + swapHash lntypes.Hash, outpoint *wire.OutPoint, confirmationHeight int32) error { + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + return q.UpdateAssetSwapHtlcTx( + ctx, sqlc.UpdateAssetSwapHtlcTxParams{ + SwapHash: swapHash[:], + HtlcTxid: outpoint.Hash[:], + HtlcVout: int32(outpoint.Index), + HtlcConfirmationHeight: confirmationHeight, + }) + }, + ) +} + +// UpdateAssetSwapOutProof updates the raw proof of the swap out in the +// database. +func (p *PostgresStore) UpdateAssetSwapOutProof(ctx context.Context, + swapHash lntypes.Hash, rawProof []byte) error { + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + return q.UpdateAssetSwapOutProof( + ctx, sqlc.UpdateAssetSwapOutProofParams{ + SwapHash: swapHash[:], + RawProofFile: rawProof, + }) + }, + ) +} + +// UpdateAssetSwapOutPreimage updates the preimage of the swap out in the +// database. +func (p *PostgresStore) UpdateAssetSwapOutPreimage(ctx context.Context, + swapHash lntypes.Hash, preimage lntypes.Preimage) error { + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + return q.UpdateAssetSwapOutPreimage( + ctx, sqlc.UpdateAssetSwapOutPreimageParams{ + SwapHash: swapHash[:], + SwapPreimage: preimage[:], + }) + }, + ) +} + +// UpdateAssetSwapOutSweepTx updates the sweep tx of the swap out in the +// database. +func (p *PostgresStore) UpdateAssetSwapOutSweepTx(ctx context.Context, + swapHash lntypes.Hash, sweepTxid chainhash.Hash, confHeight int32, + sweepPkscript []byte) error { + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + return q.UpdateAssetSwapSweepTx( + ctx, sqlc.UpdateAssetSwapSweepTxParams{ + SwapHash: swapHash[:], + SweepTxid: sweepTxid[:], + SweepConfirmationHeight: confHeight, + SweepPkscript: sweepPkscript, + }) + }, + ) +} + +// InsertAssetSwapUpdate inserts a new swap update in the database. +func (p *PostgresStore) InsertAssetSwapUpdate(ctx context.Context, + swapHash lntypes.Hash, state fsm.StateType) error { + + return p.queries.ExecTx( + ctx, &loopdb.SqliteTxOptions{}, func(q *sqlc.Queries) error { + return q.InsertAssetSwapUpdate( + ctx, sqlc.InsertAssetSwapUpdateParams{ + SwapHash: swapHash[:], + UpdateState: string(state), + UpdateTimestamp: p.clock.Now(), + }) + }, + ) +} + +// GetAllAssetOuts returns all the asset outs from the database. +func (p *PostgresStore) GetAllAssetOuts(ctx context.Context) ([]*SwapOut, error) { + dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx) + if err != nil { + return nil, err + } + + assetOuts := make([]*SwapOut, 0, len(dbAssetOuts)) + for _, dbAssetOut := range dbAssetOuts { + assetOut, err := newSwapOutFromDB( + dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap, + dbAssetOut.UpdateState, + ) + if err != nil { + return nil, err + } + assetOuts = append(assetOuts, assetOut) + } + return assetOuts, nil +} + +// GetActiveAssetOuts returns all the active asset outs from the database. +func (p *PostgresStore) GetActiveAssetOuts(ctx context.Context) ([]*SwapOut, + error) { + + dbAssetOuts, err := p.queries.GetAllAssetOutSwaps(ctx) + if err != nil { + return nil, err + } + + assetOuts := make([]*SwapOut, 0) + for _, dbAssetOut := range dbAssetOuts { + // TODO: Uncomment this when we have a way to get the finished + // states from the database. + // if IsFinishedState(fsm.StateType(dbAssetOut.UpdateState)) { + // continue + // } + + assetOut, err := newSwapOutFromDB( + dbAssetOut.AssetSwap, dbAssetOut.AssetOutSwap, + dbAssetOut.UpdateState, + ) + if err != nil { + return nil, err + } + assetOuts = append(assetOuts, assetOut) + } + + return assetOuts, nil +} + +// newSwapOutFromDB creates a new SwapOut from the databse rows. +func newSwapOutFromDB(assetSwap sqlc.AssetSwap, + assetOutSwap sqlc.AssetOutSwap, state string) ( + *SwapOut, error) { + + swapHash, err := lntypes.MakeHash(assetSwap.SwapHash) + if err != nil { + return nil, err + } + + var swapPreimage lntypes.Preimage + if assetSwap.SwapPreimage != nil { + swapPreimage, err = lntypes.MakePreimage(assetSwap.SwapPreimage) + if err != nil { + return nil, err + } + } + + senderPubkey, err := btcec.ParsePubKey(assetSwap.SenderPubkey) + if err != nil { + return nil, err + } + + receiverPubkey, err := btcec.ParsePubKey(assetSwap.ReceiverPubkey) + if err != nil { + return nil, err + } + + var htlcOutpoint *wire.OutPoint + if assetSwap.HtlcTxid != nil { + htlcHash, err := chainhash.NewHash(assetSwap.HtlcTxid) + if err != nil { + return nil, err + } + htlcOutpoint = wire.NewOutPoint( + htlcHash, uint32(assetSwap.HtlcVout), + ) + } + + var sweepOutpoint *wire.OutPoint + if assetSwap.SweepTxid != nil { + sweepHash, err := chainhash.NewHash(assetSwap.SweepTxid) + if err != nil { + return nil, err + } + sweepOutpoint = wire.NewOutPoint( + sweepHash, 0, + ) + } + + return &SwapOut{ + SwapKit: htlc.SwapKit{ + SwapHash: swapHash, + Amount: uint64(assetSwap.Amt), + SenderPubKey: senderPubkey, + ReceiverPubKey: receiverPubkey, + CsvExpiry: uint32(assetSwap.CsvExpiry), + AssetID: assetSwap.AssetID, + }, + SwapPreimage: swapPreimage, + State: fsm.StateType(state), + InitiationHeight: uint32(assetSwap.InitiationHeight), + ClientKeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily( + assetSwap.ServerKeyFamily, + ), + Index: uint32(assetSwap.ServerKeyIndex), + }, + HtlcOutPoint: htlcOutpoint, + HtlcConfirmationHeight: uint32(assetSwap.HtlcConfirmationHeight), + SweepOutpoint: sweepOutpoint, + SweepConfirmationHeight: uint32(assetSwap.SweepConfirmationHeight), + SweepPkscript: assetSwap.SweepPkscript, + RawHtlcProof: assetOutSwap.RawProofFile, + }, nil +} diff --git a/assets/store_test.go b/assets/store_test.go new file mode 100644 index 000000000..4a952fd34 --- /dev/null +++ b/assets/store_test.go @@ -0,0 +1,114 @@ +package assets + +import ( + "context" + "crypto/rand" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/assets/htlc" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/loopdb" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +var ( + defaultClientPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d") + defaultClientPubkey, _ = btcec.ParsePubKey(defaultClientPubkeyBytes) + + defaultOutpoint = &wire.OutPoint{ + Hash: chainhash.Hash{0x01}, + Index: 1, + } +) + +// TestSqlStore tests the asset swap store. +func TestSqlStore(t *testing.T) { + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + defer testDb.Close() + + store := NewPostgresStore(testDb) + + swapPreimage := getRandomPreimage() + SwapHash := swapPreimage.Hash() + + // Create a new SwapOut. + swapOut := &SwapOut{ + SwapKit: htlc.SwapKit{ + SwapHash: SwapHash, + Amount: 100, + SenderPubKey: defaultClientPubkey, + ReceiverPubKey: defaultClientPubkey, + CsvExpiry: 100, + AssetID: []byte("assetid"), + }, + SwapPreimage: swapPreimage, + State: fsm.StateType("init"), + InitiationHeight: 1, + ClientKeyLocator: keychain.KeyLocator{ + Family: 1, + Index: 1, + }, + } + + // Save the swap out in the db. + err := store.CreateAssetSwapOut(ctxb, swapOut) + require.NoError(t, err) + + // Insert a new swap out update. + err = store.InsertAssetSwapUpdate( + ctxb, SwapHash, fsm.StateType("state2"), + ) + require.NoError(t, err) + + // Try to fetch all swap outs. + swapOuts, err := store.GetAllAssetOuts(ctxb) + require.NoError(t, err) + require.Len(t, swapOuts, 1) + + // Update the htlc outpoint. + err = store.UpdateAssetSwapHtlcOutpoint( + ctxb, SwapHash, defaultOutpoint, 100, + ) + require.NoError(t, err) + + // Update the offchain payment amount. + err = store.UpdateAssetSwapOutProof( + ctxb, SwapHash, []byte("proof"), + ) + require.NoError(t, err) + + // Try to fetch all active swap outs. + activeSwapOuts, err := store.GetActiveAssetOuts(ctxb) + require.NoError(t, err) + require.Len(t, activeSwapOuts, 1) + + // TODO: Uncomment this when we have a way to get the finished + // states from the database. + // // Update the swap out state to a finished state. + // err = store.InsertAssetSwapUpdate( + // ctxb, SwapHash, fsm.StateType(FinishedStates()[0]), + // ) + // require.NoError(t, err) + + // // Try to fetch all active swap outs. + // activeSwapOuts, err = store.GetActiveAssetOuts(ctxb) + // require.NoError(t, err) + // require.Len(t, activeSwapOuts, 0) +} + +// getRandomPreimage generates a random reservation ID. +func getRandomPreimage() lntypes.Preimage { + var id lntypes.Preimage + _, err := rand.Read(id[:]) + if err != nil { + panic(err) + } + return id +} diff --git a/assets/swap_out.go b/assets/swap_out.go new file mode 100644 index 000000000..9aa12fecc --- /dev/null +++ b/assets/swap_out.go @@ -0,0 +1,107 @@ +package assets + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/assets/htlc" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/proof" + + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lntypes" +) + +// SwapOut is a struct that represents a swap out. It contains all the +// information needed to perform a swap out. +type SwapOut struct { + // We embed swapkit for all script related helpers. + htlc.SwapKit + + // SwapPreimage is the preimage of the swap, that enables spending + // the success path, it's hash is the main identifier of the swap. + SwapPreimage lntypes.Preimage + + // State is the current state of the swap. + State fsm.StateType + + // InitiationHeight is the height at which the swap was initiated. + InitiationHeight uint32 + + // ClientKeyLocator is the key locator of the clients key. + ClientKeyLocator keychain.KeyLocator + + // HtlcOutPoint is the outpoint of the htlc that was created to + // perform the swap. + HtlcOutPoint *wire.OutPoint + + // HtlcConfirmationHeight is the height at which the htlc was + // confirmed. + HtlcConfirmationHeight uint32 + + // SweepOutpoint is the outpoint of the htlc that was swept. + SweepOutpoint *wire.OutPoint + + // SweepConfirmationHeight is the height at which the sweep was + // confirmed. + SweepConfirmationHeight uint32 + + // SweepPkscript is the pkscript of the sweep transaction. + SweepPkscript []byte + + // RawHtlcProof is the raw htlc proof that we need to send to the + // receiver. We only keep this in the OutFSM struct as we don't want + // to save it in the store. + RawHtlcProof []byte +} + +// NewSwapOut creates a new swap out. +func NewSwapOut(swapHash lntypes.Hash, amt uint64, + assetId []byte, clientKeyDesc *keychain.KeyDescriptor, + senderPubkey *btcec.PublicKey, csvExpiry, initiationHeight uint32, +) *SwapOut { + + return &SwapOut{ + SwapKit: htlc.SwapKit{ + SwapHash: swapHash, + Amount: amt, + SenderPubKey: senderPubkey, + ReceiverPubKey: clientKeyDesc.PubKey, + CsvExpiry: csvExpiry, + AssetID: assetId, + }, + // TODO: Readd this when states are implemented. + //State: Init, + InitiationHeight: initiationHeight, + ClientKeyLocator: clientKeyDesc.KeyLocator, + } +} + +// genTaprootAssetRootFromProof generates the taproot asset root from the proof +// of the swap. +func (s *SwapOut) genTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, + error) { + + assetCpy := proof.Asset.Copy() + assetCpy.PrevWitnesses[0].SplitCommitment = nil + sendCommitment, err := commitment.NewAssetCommitment( + assetCpy, + ) + if err != nil { + return nil, err + } + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.NewTapCommitment( + &version, sendCommitment, + ) + if err != nil { + return nil, err + } + taprootAssetRoot := txscript.AssembleTaprootScriptTree( + assetCommitment.TapLeaf(), + ).RootNode.TapHash() + + return taprootAssetRoot[:], nil +} diff --git a/assets/tapkit.go b/assets/tapkit.go new file mode 100644 index 000000000..8cb1b6944 --- /dev/null +++ b/assets/tapkit.go @@ -0,0 +1,130 @@ +package assets + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil/psbt" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/assets/htlc" + "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/commitment" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/tappsbt" + "github.com/lightninglabs/taproot-assets/tapsend" +) + +// GenTaprootAssetRootFromProof generates the taproot asset root from the proof +// of the swap. +func GenTaprootAssetRootFromProof(proof *proof.Proof) ([]byte, error) { + assetCopy := proof.Asset.CopySpendTemplate() + + version := commitment.TapCommitmentV2 + assetCommitment, err := commitment.FromAssets(&version, assetCopy) + if err != nil { + return nil, err + } + + assetCommitment, err = commitment.TrimSplitWitnesses( + &version, assetCommitment, + ) + if err != nil { + return nil, err + } + + taprootAssetRoot := assetCommitment.TapscriptRoot(nil) + + return taprootAssetRoot[:], nil +} + +// CreateOpTrueSweepVpkt creates a VPacket that sweeps the outputs associated +// with the passed in proofs, given that their TAP script is a simple OP_TRUE. +func CreateOpTrueSweepVpkt(ctx context.Context, proofs []*proof.Proof, + addr *address.Tap, chainParams *address.ChainParams) ( + *tappsbt.VPacket, error) { + + sweepVpkt, err := tappsbt.FromProofs(proofs, chainParams, tappsbt.V1) + if err != nil { + return nil, err + } + + total := uint64(0) + for i, proof := range proofs { + inputKey := proof.InclusionProof.InternalKey + + sweepVpkt.Inputs[i].Anchor.Bip32Derivation = + []*psbt.Bip32Derivation{ + { + PubKey: inputKey.SerializeCompressed(), + }, + } + sweepVpkt.Inputs[i].Anchor.TrBip32Derivation = + []*psbt.TaprootBip32Derivation{ + { + XOnlyPubKey: schnorr.SerializePubKey( + inputKey, + ), + }, + } + + total += proof.Asset.Amount + } + + // Sanity check that the amount that we're attempting to sweep matches + // the address amount. + if total != addr.Amount { + return nil, fmt.Errorf("total amount of proofs does not " + + "match the amount of the address") + } + + sweepVpkt.Outputs = append(sweepVpkt.Outputs, &tappsbt.VOutput{ + AssetVersion: addr.AssetVersion, + Amount: addr.Amount, + Interactive: true, + AnchorOutputIndex: 0, + ScriptKey: asset.NewScriptKey( + &addr.ScriptKey, + ), + AnchorOutputInternalKey: &addr.InternalKey, + AnchorOutputTapscriptSibling: addr.TapscriptSibling, + ProofDeliveryAddress: &addr.ProofCourierAddr, + }) + + err = tapsend.PrepareOutputAssets(ctx, sweepVpkt) + if err != nil { + return nil, err + } + + _, _, _, controlBlock, err := htlc.CreateOpTrueLeaf() + if err != nil { + return nil, err + } + + controlBlockBytes, err := controlBlock.ToBytes() + if err != nil { + return nil, err + } + + opTrueScript, err := htlc.GetOpTrueScript() + if err != nil { + return nil, err + } + + witness := wire.TxWitness{ + opTrueScript, + controlBlockBytes, + } + + firstPrevWitness := &sweepVpkt.Outputs[0].Asset.PrevWitnesses[0] + + if sweepVpkt.Outputs[0].Asset.HasSplitCommitmentWitness() { + rootAsset := firstPrevWitness.SplitCommitment.RootAsset + firstPrevWitness = &rootAsset.PrevWitnesses[0] + } + + firstPrevWitness.TxWitness = witness + + return sweepVpkt, nil +} diff --git a/loopdb/sqlc/asset_swaps.sql.go b/loopdb/sqlc/asset_swaps.sql.go new file mode 100644 index 000000000..29ce0b4c9 --- /dev/null +++ b/loopdb/sqlc/asset_swaps.sql.go @@ -0,0 +1,314 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: asset_swaps.sql + +package sqlc + +import ( + "context" + "time" +) + +const createAssetOutSwap = `-- name: CreateAssetOutSwap :exec +INSERT INTO asset_out_swaps ( + swap_hash +) VALUES ( + $1 +) +` + +func (q *Queries) CreateAssetOutSwap(ctx context.Context, swapHash []byte) error { + _, err := q.db.ExecContext(ctx, createAssetOutSwap, swapHash) + return err +} + +const createAssetSwap = `-- name: CreateAssetSwap :exec + INSERT INTO asset_swaps( + swap_hash, + swap_preimage, + asset_id, + amt, + sender_pubkey, + receiver_pubkey, + csv_expiry, + initiation_height, + created_time, + server_key_family, + server_key_index + ) + VALUES + ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 + ) +` + +type CreateAssetSwapParams struct { + SwapHash []byte + SwapPreimage []byte + AssetID []byte + Amt int64 + SenderPubkey []byte + ReceiverPubkey []byte + CsvExpiry int32 + InitiationHeight int32 + CreatedTime time.Time + ServerKeyFamily int64 + ServerKeyIndex int64 +} + +func (q *Queries) CreateAssetSwap(ctx context.Context, arg CreateAssetSwapParams) error { + _, err := q.db.ExecContext(ctx, createAssetSwap, + arg.SwapHash, + arg.SwapPreimage, + arg.AssetID, + arg.Amt, + arg.SenderPubkey, + arg.ReceiverPubkey, + arg.CsvExpiry, + arg.InitiationHeight, + arg.CreatedTime, + arg.ServerKeyFamily, + arg.ServerKeyIndex, + ) + return err +} + +const getAllAssetOutSwaps = `-- name: GetAllAssetOutSwaps :many +SELECT DISTINCT + asw.id, asw.swap_hash, asw.swap_preimage, asw.asset_id, asw.amt, asw.sender_pubkey, asw.receiver_pubkey, asw.csv_expiry, asw.server_key_family, asw.server_key_index, asw.initiation_height, asw.created_time, asw.htlc_confirmation_height, asw.htlc_txid, asw.htlc_vout, asw.sweep_txid, asw.sweep_confirmation_height, asw.sweep_pkscript, + aos.swap_hash, aos.raw_proof_file, + asu.update_state +FROM + asset_swaps asw +INNER JOIN ( + SELECT + swap_hash, + update_state, + ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn + FROM + asset_swaps_updates +) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1 +INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash +ORDER BY + asw.id +` + +type GetAllAssetOutSwapsRow struct { + AssetSwap AssetSwap + AssetOutSwap AssetOutSwap + UpdateState string +} + +func (q *Queries) GetAllAssetOutSwaps(ctx context.Context) ([]GetAllAssetOutSwapsRow, error) { + rows, err := q.db.QueryContext(ctx, getAllAssetOutSwaps) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllAssetOutSwapsRow + for rows.Next() { + var i GetAllAssetOutSwapsRow + if err := rows.Scan( + &i.AssetSwap.ID, + &i.AssetSwap.SwapHash, + &i.AssetSwap.SwapPreimage, + &i.AssetSwap.AssetID, + &i.AssetSwap.Amt, + &i.AssetSwap.SenderPubkey, + &i.AssetSwap.ReceiverPubkey, + &i.AssetSwap.CsvExpiry, + &i.AssetSwap.ServerKeyFamily, + &i.AssetSwap.ServerKeyIndex, + &i.AssetSwap.InitiationHeight, + &i.AssetSwap.CreatedTime, + &i.AssetSwap.HtlcConfirmationHeight, + &i.AssetSwap.HtlcTxid, + &i.AssetSwap.HtlcVout, + &i.AssetSwap.SweepTxid, + &i.AssetSwap.SweepConfirmationHeight, + &i.AssetSwap.SweepPkscript, + &i.AssetOutSwap.SwapHash, + &i.AssetOutSwap.RawProofFile, + &i.UpdateState, + ); 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 getAssetOutSwap = `-- name: GetAssetOutSwap :one +SELECT DISTINCT + asw.id, asw.swap_hash, asw.swap_preimage, asw.asset_id, asw.amt, asw.sender_pubkey, asw.receiver_pubkey, asw.csv_expiry, asw.server_key_family, asw.server_key_index, asw.initiation_height, asw.created_time, asw.htlc_confirmation_height, asw.htlc_txid, asw.htlc_vout, asw.sweep_txid, asw.sweep_confirmation_height, asw.sweep_pkscript, + aos.swap_hash, aos.raw_proof_file, + asu.update_state +FROM + asset_swaps asw +INNER JOIN ( + SELECT + swap_hash, + update_state, + ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn + FROM + asset_swaps_updates +) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1 +INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash +WHERE + asw.swap_hash = $1 +` + +type GetAssetOutSwapRow struct { + AssetSwap AssetSwap + AssetOutSwap AssetOutSwap + UpdateState string +} + +func (q *Queries) GetAssetOutSwap(ctx context.Context, swapHash []byte) (GetAssetOutSwapRow, error) { + row := q.db.QueryRowContext(ctx, getAssetOutSwap, swapHash) + var i GetAssetOutSwapRow + err := row.Scan( + &i.AssetSwap.ID, + &i.AssetSwap.SwapHash, + &i.AssetSwap.SwapPreimage, + &i.AssetSwap.AssetID, + &i.AssetSwap.Amt, + &i.AssetSwap.SenderPubkey, + &i.AssetSwap.ReceiverPubkey, + &i.AssetSwap.CsvExpiry, + &i.AssetSwap.ServerKeyFamily, + &i.AssetSwap.ServerKeyIndex, + &i.AssetSwap.InitiationHeight, + &i.AssetSwap.CreatedTime, + &i.AssetSwap.HtlcConfirmationHeight, + &i.AssetSwap.HtlcTxid, + &i.AssetSwap.HtlcVout, + &i.AssetSwap.SweepTxid, + &i.AssetSwap.SweepConfirmationHeight, + &i.AssetSwap.SweepPkscript, + &i.AssetOutSwap.SwapHash, + &i.AssetOutSwap.RawProofFile, + &i.UpdateState, + ) + return i, err +} + +const insertAssetSwapUpdate = `-- name: InsertAssetSwapUpdate :exec +INSERT INTO asset_swaps_updates ( + swap_hash, + update_state, + update_timestamp +) VALUES ( + $1, + $2, + $3 +) +` + +type InsertAssetSwapUpdateParams struct { + SwapHash []byte + UpdateState string + UpdateTimestamp time.Time +} + +func (q *Queries) InsertAssetSwapUpdate(ctx context.Context, arg InsertAssetSwapUpdateParams) error { + _, err := q.db.ExecContext(ctx, insertAssetSwapUpdate, arg.SwapHash, arg.UpdateState, arg.UpdateTimestamp) + return err +} + +const updateAssetSwapHtlcTx = `-- name: UpdateAssetSwapHtlcTx :exec +UPDATE asset_swaps +SET + htlc_confirmation_height = $2, + htlc_txid = $3, + htlc_vout = $4 +WHERE + asset_swaps.swap_hash = $1 +` + +type UpdateAssetSwapHtlcTxParams struct { + SwapHash []byte + HtlcConfirmationHeight int32 + HtlcTxid []byte + HtlcVout int32 +} + +func (q *Queries) UpdateAssetSwapHtlcTx(ctx context.Context, arg UpdateAssetSwapHtlcTxParams) error { + _, err := q.db.ExecContext(ctx, updateAssetSwapHtlcTx, + arg.SwapHash, + arg.HtlcConfirmationHeight, + arg.HtlcTxid, + arg.HtlcVout, + ) + return err +} + +const updateAssetSwapOutPreimage = `-- name: UpdateAssetSwapOutPreimage :exec +UPDATE asset_swaps +SET + swap_preimage = $2 +WHERE + asset_swaps.swap_hash = $1 +` + +type UpdateAssetSwapOutPreimageParams struct { + SwapHash []byte + SwapPreimage []byte +} + +func (q *Queries) UpdateAssetSwapOutPreimage(ctx context.Context, arg UpdateAssetSwapOutPreimageParams) error { + _, err := q.db.ExecContext(ctx, updateAssetSwapOutPreimage, arg.SwapHash, arg.SwapPreimage) + return err +} + +const updateAssetSwapOutProof = `-- name: UpdateAssetSwapOutProof :exec +UPDATE asset_out_swaps +SET + raw_proof_file = $2 +WHERE + asset_out_swaps.swap_hash = $1 +` + +type UpdateAssetSwapOutProofParams struct { + SwapHash []byte + RawProofFile []byte +} + +func (q *Queries) UpdateAssetSwapOutProof(ctx context.Context, arg UpdateAssetSwapOutProofParams) error { + _, err := q.db.ExecContext(ctx, updateAssetSwapOutProof, arg.SwapHash, arg.RawProofFile) + return err +} + +const updateAssetSwapSweepTx = `-- name: UpdateAssetSwapSweepTx :exec +UPDATE asset_swaps +SET + sweep_confirmation_height = $2, + sweep_txid = $3, + sweep_pkscript = $4 +WHERE + asset_swaps.swap_hash = $1 +` + +type UpdateAssetSwapSweepTxParams struct { + SwapHash []byte + SweepConfirmationHeight int32 + SweepTxid []byte + SweepPkscript []byte +} + +func (q *Queries) UpdateAssetSwapSweepTx(ctx context.Context, arg UpdateAssetSwapSweepTxParams) error { + _, err := q.db.ExecContext(ctx, updateAssetSwapSweepTx, + arg.SwapHash, + arg.SweepConfirmationHeight, + arg.SweepTxid, + arg.SweepPkscript, + ) + return err +} diff --git a/loopdb/sqlc/migrations/000016_asset_swaps.down.sql b/loopdb/sqlc/migrations/000016_asset_swaps.down.sql new file mode 100644 index 000000000..93b3502c2 --- /dev/null +++ b/loopdb/sqlc/migrations/000016_asset_swaps.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS asset_out_swaps_swap_hash_idx; +DROP TABLE IF EXISTS asset_out_swaps; +DROP INDEX IF EXISTS asset_swaps_updates_swap_hash_idx; +DROP TABLE IF EXISTS asset_swaps; +DROP TABLE IF EXISTS asset_swaps; \ No newline at end of file diff --git a/loopdb/sqlc/migrations/000016_asset_swaps.up.sql b/loopdb/sqlc/migrations/000016_asset_swaps.up.sql new file mode 100644 index 000000000..3f48e9d16 --- /dev/null +++ b/loopdb/sqlc/migrations/000016_asset_swaps.up.sql @@ -0,0 +1,85 @@ +CREATE TABLE IF NOT EXISTS asset_swaps ( + --- id is the autoincrementing primary key. + id INTEGER PRIMARY KEY, + + -- swap_hash is the randomly generated hash of the swap, which is used + -- as the swap identifier for the clients. + swap_hash BLOB NOT NULL UNIQUE, + + -- swap_preimage is the preimage of the swap. + swap_preimage BLOB, + + -- asset_id is the identifier of the asset being swapped. + asset_id BLOB NOT NULL, + + -- amt is the requested amount to be swapped. + amt BIGINT NOT NULL, + + -- sender_pubkey is the pubkey of the sender. + sender_pubkey BLOB NOT NULL, + + -- receiver_pubkey is the pubkey of the receiver. + receiver_pubkey BLOB NOT NULL, + + -- csv_expiry is the expiry of the swap. + csv_expiry INTEGER NOT NULL, + + -- server_key_family is the family of key being identified. + server_key_family BIGINT NOT NULL, + + -- server_key_index is the precise index of the key being identified. + server_key_index BIGINT NOT NULL, + + -- initiation_height is the height at which the swap was initiated. + initiation_height INTEGER NOT NULL, + + -- created_time is the time at which the swap was created. + created_time TIMESTAMP NOT NULL, + + -- htlc_confirmation_height is the height at which the swap was confirmed. + htlc_confirmation_height INTEGER NOT NULL DEFAULT(0), + + -- htlc_txid is the txid of the confirmation transaction. + htlc_txid BLOB, + + -- htlc_vout is the vout of the confirmation transaction. + htlc_vout INTEGER NOT NULL DEFAULT (0), + + -- sweep_txid is the txid of the sweep transaction. + sweep_txid BLOB, + + -- sweep_confirmation_height is the height at which the swap was swept. + sweep_confirmation_height INTEGER NOT NULL DEFAULT(0), + + sweep_pkscript BLOB +); + + +CREATE TABLE IF NOT EXISTS asset_swaps_updates ( + -- id is auto incremented for each update. + id INTEGER PRIMARY KEY, + + -- swap_hash is the hash of the swap that this update is for. + swap_hash BLOB NOT NULL REFERENCES asset_swaps(swap_hash), + + -- update_state is the state of the swap at the time of the update. + update_state TEXT NOT NULL, + + -- update_timestamp is the time at which the update was created. + update_timestamp TIMESTAMP NOT NULL +); + + +CREATE INDEX IF NOT EXISTS asset_swaps_updates_swap_hash_idx ON asset_swaps_updates(swap_hash); + + +CREATE TABLE IF NOT EXISTS asset_out_swaps ( + -- swap_hash is the identifier of the swap. + swap_hash BLOB PRIMARY KEY REFERENCES asset_swaps(swap_hash), + + -- raw_proof_file is the file containing the raw proof. + raw_proof_file BLOB +); + +CREATE INDEX IF NOT EXISTS asset_out_swaps_swap_hash_idx ON asset_out_swaps(swap_hash); + diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index c71f02392..a1feaae91 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -9,6 +9,39 @@ import ( "time" ) +type AssetOutSwap struct { + SwapHash []byte + RawProofFile []byte +} + +type AssetSwap struct { + ID int32 + SwapHash []byte + SwapPreimage []byte + AssetID []byte + Amt int64 + SenderPubkey []byte + ReceiverPubkey []byte + CsvExpiry int32 + ServerKeyFamily int64 + ServerKeyIndex int64 + InitiationHeight int32 + CreatedTime time.Time + HtlcConfirmationHeight int32 + HtlcTxid []byte + HtlcVout int32 + SweepTxid []byte + SweepConfirmationHeight int32 + SweepPkscript []byte +} + +type AssetSwapsUpdate struct { + ID int32 + SwapHash []byte + UpdateState string + UpdateTimestamp time.Time +} + type Deposit struct { ID int32 DepositID []byte diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 9b600727e..245dc7bb4 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -13,6 +13,8 @@ type Querier interface { AllDeposits(ctx context.Context) ([]Deposit, error) AllStaticAddresses(ctx context.Context) ([]StaticAddress, error) ConfirmBatch(ctx context.Context, id int32) error + CreateAssetOutSwap(ctx context.Context, swapHash []byte) error + CreateAssetSwap(ctx context.Context, arg CreateAssetSwapParams) error CreateDeposit(ctx context.Context, arg CreateDepositParams) error CreateReservation(ctx context.Context, arg CreateReservationParams) error CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error @@ -20,7 +22,9 @@ type Querier interface { CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error DropBatch(ctx context.Context, id int32) error FetchLiquidityParams(ctx context.Context) ([]byte, error) + GetAllAssetOutSwaps(ctx context.Context) ([]GetAllAssetOutSwapsRow, error) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) + GetAssetOutSwap(ctx context.Context, swapHash []byte) (GetAssetOutSwapRow, 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) @@ -47,6 +51,7 @@ type Querier interface { GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error) GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) ([][]byte, error) GetWithdrawalIDByDepositID(ctx context.Context, depositID []byte) ([]byte, error) + InsertAssetSwapUpdate(ctx context.Context, arg InsertAssetSwapUpdateParams) error InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error) InsertDepositUpdate(ctx context.Context, arg InsertDepositUpdateParams) error InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error @@ -63,6 +68,10 @@ type Querier interface { InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error IsStored(ctx context.Context, swapHash []byte) (bool, error) OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + UpdateAssetSwapHtlcTx(ctx context.Context, arg UpdateAssetSwapHtlcTxParams) error + UpdateAssetSwapOutPreimage(ctx context.Context, arg UpdateAssetSwapOutPreimageParams) error + UpdateAssetSwapOutProof(ctx context.Context, arg UpdateAssetSwapOutProofParams) error + UpdateAssetSwapSweepTx(ctx context.Context, arg UpdateAssetSwapSweepTxParams) error UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error diff --git a/loopdb/sqlc/queries/asset_swaps.sql b/loopdb/sqlc/queries/asset_swaps.sql new file mode 100644 index 000000000..98201c4b4 --- /dev/null +++ b/loopdb/sqlc/queries/asset_swaps.sql @@ -0,0 +1,108 @@ +-- name: CreateAssetSwap :exec + INSERT INTO asset_swaps( + swap_hash, + swap_preimage, + asset_id, + amt, + sender_pubkey, + receiver_pubkey, + csv_expiry, + initiation_height, + created_time, + server_key_family, + server_key_index + ) + VALUES + ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 + ); + +-- name: CreateAssetOutSwap :exec +INSERT INTO asset_out_swaps ( + swap_hash +) VALUES ( + $1 +); + +-- name: UpdateAssetSwapHtlcTx :exec +UPDATE asset_swaps +SET + htlc_confirmation_height = $2, + htlc_txid = $3, + htlc_vout = $4 +WHERE + asset_swaps.swap_hash = $1; + +-- name: UpdateAssetSwapOutProof :exec +UPDATE asset_out_swaps +SET + raw_proof_file = $2 +WHERE + asset_out_swaps.swap_hash = $1; + +-- name: UpdateAssetSwapOutPreimage :exec +UPDATE asset_swaps +SET + swap_preimage = $2 +WHERE + asset_swaps.swap_hash = $1; + +-- name: UpdateAssetSwapSweepTx :exec +UPDATE asset_swaps +SET + sweep_confirmation_height = $2, + sweep_txid = $3, + sweep_pkscript = $4 +WHERE + asset_swaps.swap_hash = $1; + +-- name: InsertAssetSwapUpdate :exec +INSERT INTO asset_swaps_updates ( + swap_hash, + update_state, + update_timestamp +) VALUES ( + $1, + $2, + $3 +); + + +-- name: GetAssetOutSwap :one +SELECT DISTINCT + sqlc.embed(asw), + sqlc.embed(aos), + asu.update_state +FROM + asset_swaps asw +INNER JOIN ( + SELECT + swap_hash, + update_state, + ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn + FROM + asset_swaps_updates +) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1 +INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash +WHERE + asw.swap_hash = $1; + +-- name: GetAllAssetOutSwaps :many +SELECT DISTINCT + sqlc.embed(asw), + sqlc.embed(aos), + asu.update_state +FROM + asset_swaps asw +INNER JOIN ( + SELECT + swap_hash, + update_state, + ROW_NUMBER() OVER(PARTITION BY swap_hash ORDER BY id DESC) as rn + FROM + asset_swaps_updates +) asu ON asw.swap_hash = asu.swap_hash AND asu.rn = 1 +INNER JOIN asset_out_swaps aos ON asw.swap_hash = aos.swap_hash +ORDER BY + asw.id; + diff --git a/utils/listeners.go b/utils/listeners.go new file mode 100644 index 000000000..54716d8d3 --- /dev/null +++ b/utils/listeners.go @@ -0,0 +1,257 @@ +package utils + +import ( + "context" + "sync" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lntypes" +) + +// ExpiryManager is a manager for block height expiry events. +type ExpiryManager struct { + chainNotifier lndclient.ChainNotifierClient + + expiryHeightMap map[[32]byte]int32 + expiryFuncMap map[[32]byte]func() + + currentBlockHeight int32 + + sync.Mutex +} + +// NewExpiryManager creates a new expiry manager. +func NewExpiryManager( + chainNotifier lndclient.ChainNotifierClient) *ExpiryManager { + + return &ExpiryManager{ + chainNotifier: chainNotifier, + expiryHeightMap: make(map[[32]byte]int32), + expiryFuncMap: make(map[[32]byte]func()), + } +} + +// Start starts the expiry manager and listens for block height notifications. +func (e *ExpiryManager) Start(ctx context.Context, startingBlockHeight int32, +) error { + + e.Lock() + e.currentBlockHeight = startingBlockHeight + e.Unlock() + + log.Debugf("Starting expiry manager at height %d", startingBlockHeight) + defer log.Debugf("Expiry manager stopped") + + blockHeightChan, errChan, err := e.chainNotifier.RegisterBlockEpochNtfn( + ctx, + ) + if err != nil { + return err + } + + for { + select { + case blockHeight := <-blockHeightChan: + + log.Debugf("Received block height %d", blockHeight) + + e.Lock() + e.currentBlockHeight = blockHeight + e.Unlock() + + e.checkExpiry(blockHeight) + + case err := <-errChan: + log.Debugf("Expiry manager error") + return err + + case <-ctx.Done(): + log.Debugf("Expiry manager stopped") + return nil + } + } +} + +// GetBlockHeight returns the current block height. +func (e *ExpiryManager) GetBlockHeight() int32 { + e.Lock() + defer e.Unlock() + + return e.currentBlockHeight +} + +// checkExpiry checks if any swaps have expired and calls the expiry function if +// they have. +func (e *ExpiryManager) checkExpiry(blockHeight int32) { + e.Lock() + defer e.Unlock() + + for swapHash, expiryHeight := range e.expiryHeightMap { + if blockHeight >= expiryHeight { + expiryFunc := e.expiryFuncMap[swapHash] + go expiryFunc() + + delete(e.expiryHeightMap, swapHash) + delete(e.expiryFuncMap, swapHash) + } + } +} + +// SubscribeExpiry subscribes to an expiry event for a swap. If the expiry height +// has already been reached, the expiryFunc is not called and the function +// returns true. Otherwise, the expiryFunc is called when the expiry height is +// reached and the function returns false. +func (e *ExpiryManager) SubscribeExpiry(swapHash [32]byte, + expiryHeight int32, expiryFunc func()) bool { + + e.Lock() + defer e.Unlock() + + if e.currentBlockHeight >= expiryHeight { + return true + } + + log.Debugf("Subscribing to expiry for swap %x at height %d", + swapHash, expiryHeight) + + e.expiryHeightMap[swapHash] = expiryHeight + e.expiryFuncMap[swapHash] = expiryFunc + + return false +} + +// SubscribeInvoiceManager is a manager for invoice subscription events. +type SubscribeInvoiceManager struct { + invoicesClient lndclient.InvoicesClient + + subscribers map[[32]byte]struct{} + + sync.Mutex +} + +// NewSubscribeInvoiceManager creates a new subscribe invoice manager. +func NewSubscribeInvoiceManager( + invoicesClient lndclient.InvoicesClient) *SubscribeInvoiceManager { + + return &SubscribeInvoiceManager{ + invoicesClient: invoicesClient, + subscribers: make(map[[32]byte]struct{}), + } +} + +// SubscribeInvoice subscribes to invoice events for a swap hash. The update +// callback is called when the invoice is updated and the error callback is +// called when an error occurs. +func (s *SubscribeInvoiceManager) SubscribeInvoice(ctx context.Context, + invoiceHash lntypes.Hash, callback func(lndclient.InvoiceUpdate, error), +) error { + + s.Lock() + defer s.Unlock() + // If we already have a subscriber for this swap hash, return early. + if _, ok := s.subscribers[invoiceHash]; ok { + return nil + } + + log.Debugf("Subscribing to invoice %v", invoiceHash) + + updateChan, errChan, err := s.invoicesClient.SubscribeSingleInvoice( + ctx, invoiceHash, + ) + if err != nil { + return err + } + + s.subscribers[invoiceHash] = struct{}{} + + go func() { + for { + select { + case update := <-updateChan: + callback(update, nil) + + case err := <-errChan: + callback(lndclient.InvoiceUpdate{}, err) + delete(s.subscribers, invoiceHash) + return + + case <-ctx.Done(): + delete(s.subscribers, invoiceHash) + return + } + } + }() + + return nil +} + +// TxSubscribeConfirmationManager is a manager for transaction confirmation +// subscription events. +type TxSubscribeConfirmationManager struct { + chainNotifier lndclient.ChainNotifierClient + + subscribers map[[32]byte]struct{} + + sync.Mutex +} + +// NewTxSubscribeConfirmationManager creates a new transaction confirmation +// subscription manager. +func NewTxSubscribeConfirmationManager(chainNtfn lndclient.ChainNotifierClient, +) *TxSubscribeConfirmationManager { + + return &TxSubscribeConfirmationManager{ + chainNotifier: chainNtfn, + subscribers: make(map[[32]byte]struct{}), + } +} + +// SubscribeTxConfirmation subscribes to transaction confirmation events for a +// swap hash. The callback is called when the transaction is confirmed or an +// error occurs. +func (t *TxSubscribeConfirmationManager) SubscribeTxConfirmation( + ctx context.Context, swapHash lntypes.Hash, txid *chainhash.Hash, + pkscript []byte, numConfs int32, heightHint int32, + cb func(*chainntnfs.TxConfirmation, error)) error { + + t.Lock() + defer t.Unlock() + + // If we already have a subscriber for this swap hash, return early. + if _, ok := t.subscribers[swapHash]; ok { + return nil + } + + log.Debugf("Subscribing to tx confirmation for swap %v", swapHash) + + confChan, errChan, err := t.chainNotifier.RegisterConfirmationsNtfn( + ctx, txid, pkscript, numConfs, heightHint, + ) + if err != nil { + return err + } + + t.subscribers[swapHash] = struct{}{} + + go func() { + for { + select { + case conf := <-confChan: + cb(conf, nil) + + case err := <-errChan: + cb(nil, err) + delete(t.subscribers, swapHash) + return + + case <-ctx.Done(): + delete(t.subscribers, swapHash) + return + } + } + }() + + return nil +} diff --git a/utils/log.go b/utils/log.go new file mode 100644 index 000000000..fcefd75c4 --- /dev/null +++ b/utils/log.go @@ -0,0 +1,26 @@ +package utils + +import ( + "github.com/btcsuite/btclog/v2" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "UTILS" + +// 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 +}