muun-recovery/vendor/github.com/muun/libwallet/incoming_swap.go

471 lines
13 KiB
Go

package libwallet
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/muun/libwallet/btcsuitew/txscriptw"
"github.com/muun/libwallet/hdpath"
"github.com/muun/libwallet/sphinx"
"github.com/muun/libwallet/walletdb"
)
type IncomingSwap struct {
Htlc *IncomingSwapHtlc
SphinxPacket []byte
PaymentHash []byte
PaymentAmountSat int64
CollectSat int64
}
type IncomingSwapHtlc struct {
HtlcTx []byte
ExpirationHeight int64
SwapServerPublicKey []byte
}
type IncomingSwapFulfillmentData struct {
FulfillmentTx []byte
MuunSignature []byte
OutputVersion int // unused
OutputPath string // unused
MerkleTree []byte // unused
HtlcBlock []byte // unused
BlockHeight int64 // unused
ConfirmationTarget int64 // to validate fee rate, unused for now
}
type IncomingSwapFulfillmentResult struct {
FulfillmentTx []byte
Preimage []byte
}
func (s *IncomingSwap) getInvoice() (*walletdb.Invoice, error) {
db, err := openDB()
if err != nil {
return nil, err
}
defer db.Close()
return db.FindByPaymentHash(s.PaymentHash)
}
// VerifyFulfillable checks that an incoming swap is fulfillable.
func (s *IncomingSwap) VerifyFulfillable(userKey *HDPrivateKey, net *Network) error {
paymentHash := s.PaymentHash
if len(paymentHash) != 32 {
return fmt.Errorf("VerifyFulfillable: received invalid hash len %v", len(paymentHash))
}
// Lookup invoice data matching this HTLC using the payment hash
invoice, err := s.getInvoice()
if err != nil {
return fmt.Errorf("VerifyFulfillable: could not find invoice data for payment hash: %w", err)
}
parentPath, err := hdpath.Parse(invoice.KeyPath)
if err != nil {
return fmt.Errorf("VerifyFulfillable: invoice key path is not valid: %v", invoice.KeyPath)
}
identityKeyPath := parentPath.Child(identityKeyChildIndex)
nodeHDKey, err := userKey.DeriveTo(identityKeyPath.String())
if err != nil {
return fmt.Errorf("VerifyFulfillable: failed to derive key: %w", err)
}
nodeKey, err := nodeHDKey.key.ECPrivKey()
if err != nil {
return fmt.Errorf("VerifyFulfillable: failed to get priv key: %w", err)
}
// implementation is allowed to send a few extra sats
if invoice.AmountSat != 0 && invoice.AmountSat > s.PaymentAmountSat {
return fmt.Errorf("VerifyFulfillable: payment amount (%v) does not match invoice amount (%v)",
s.PaymentAmountSat, invoice.AmountSat)
}
if len(s.SphinxPacket) == 0 {
return nil
}
err = sphinx.Validate(
s.SphinxPacket,
paymentHash,
invoice.PaymentSecret,
nodeKey,
0, // This is used internally by the sphinx decoder but it's not needed
lnwire.MilliSatoshi(uint64(s.PaymentAmountSat)*1000),
net.network,
)
if err != nil {
return fmt.Errorf("VerifyFulfillable: invalid sphinx: %w", err)
}
return nil
}
// Fulfill validates and creates a fulfillment tx for the incoming swap.
// It returns the fullfillment tx and the preimage.
func (s *IncomingSwap) Fulfill(
data *IncomingSwapFulfillmentData,
userKey *HDPrivateKey, muunKey *HDPublicKey,
net *Network) (*IncomingSwapFulfillmentResult, error) {
if s.Htlc == nil {
return nil, fmt.Errorf("Fulfill: missing swap htlc data")
}
err := s.VerifyFulfillable(userKey, net)
if err != nil {
return nil, err
}
// Validate the fullfillment tx proposed by Muun.
tx := wire.MsgTx{}
err = tx.DeserializeNoWitness(bytes.NewReader(data.FulfillmentTx))
if err != nil {
return nil, fmt.Errorf("Fulfill: could not deserialize fulfillment tx: %w", err)
}
if len(tx.TxIn) != 1 {
return nil, fmt.Errorf("Fulfill: expected fulfillment tx to have exactly 1 input, found %d", len(tx.TxIn))
}
if len(tx.TxOut) != 1 {
return nil, fmt.Errorf("Fulfill: expected fulfillment tx to have exactly 1 output, found %d", len(tx.TxOut))
}
// Lookup invoice data matching this HTLC using the payment hash
invoice, err := s.getInvoice()
if err != nil {
return nil, fmt.Errorf("Fulfill: could not find invoice data for payment hash: %w", err)
}
// Sign the htlc input (there is only one, at index 0)
coin := coinIncomingSwap{
Network: net.network,
MuunSignature: data.MuunSignature,
Sphinx: s.SphinxPacket,
HtlcTx: s.Htlc.HtlcTx,
PaymentHash256: s.PaymentHash,
SwapServerPublicKey: []byte(s.Htlc.SwapServerPublicKey),
ExpirationHeight: s.Htlc.ExpirationHeight,
VerifyOutputAmount: true,
Collect: btcutil.Amount(s.CollectSat),
}
err = coin.SignInput(0, &tx, userKey, muunKey)
if err != nil {
return nil, err
}
// Serialize and return the signed fulfillment tx
var buf bytes.Buffer
err = tx.Serialize(&buf)
if err != nil {
return nil, fmt.Errorf("Fulfill: could not serialize fulfillment tx: %w", err)
}
return &IncomingSwapFulfillmentResult{
FulfillmentTx: buf.Bytes(),
Preimage: invoice.Preimage,
}, nil
}
// FulfillFullDebt gives the preimage matching a payment hash if we have it
func (s *IncomingSwap) FulfillFullDebt() (*IncomingSwapFulfillmentResult, error) {
// Lookup invoice data matching this HTLC using the payment hash
db, err := openDB()
if err != nil {
return nil, err
}
defer db.Close()
secrets, err := db.FindByPaymentHash(s.PaymentHash)
if err != nil {
return nil, fmt.Errorf("FulfillFullDebt: could not find invoice data for payment hash: %w", err)
}
return &IncomingSwapFulfillmentResult{
FulfillmentTx: nil,
Preimage: secrets.Preimage,
}, nil
}
type coinIncomingSwap struct {
Network *chaincfg.Params
MuunSignature []byte
Sphinx []byte
HtlcTx []byte
PaymentHash256 []byte
SwapServerPublicKey []byte
ExpirationHeight int64
VerifyOutputAmount bool // used only for fulfilling swaps through IncomingSwap
Collect btcutil.Amount
}
func (c *coinIncomingSwap) SignInput(index int, tx *wire.MsgTx, userKey *HDPrivateKey, muunKey *HDPublicKey) error {
// Deserialize the HTLC transaction
htlcTx := wire.MsgTx{}
err := htlcTx.Deserialize(bytes.NewReader(c.HtlcTx))
if err != nil {
return fmt.Errorf("could not deserialize htlc tx: %w", err)
}
// Lookup invoice data matching this HTLC using the payment hash
db, err := openDB()
if err != nil {
return err
}
defer db.Close()
secrets, err := db.FindByPaymentHash(c.PaymentHash256)
if err != nil {
return fmt.Errorf("could not find invoice data for payment hash: %w", err)
}
parentPath, err := hdpath.Parse(secrets.KeyPath)
if err != nil {
return fmt.Errorf("invalid invoice key path: %w", err)
}
// Recreate the HTLC script to verify it matches the transaction. For this
// we must derive the keys used in the HTLC script
htlcKeyPath := parentPath.Child(htlcKeyChildIndex)
// Derive first the private key, which we are going to use for signing later
userPrivateKey, err := userKey.DeriveTo(htlcKeyPath.String())
if err != nil {
return err
}
userPublicKey := userPrivateKey.PublicKey()
muunPublicKey, err := muunKey.DeriveTo(htlcKeyPath.String())
if err != nil {
return err
}
htlcScript, err := c.createHtlcScript(userPublicKey, muunPublicKey)
if err != nil {
return fmt.Errorf("could not create htlc script: %w", err)
}
// Try to find the script we just built inside the HTLC output scripts
htlcOutputIndex, err := c.findHtlcOutputIndex(&htlcTx, htlcScript)
if err != nil {
return err
}
// Next, we must validate the sphinx data. We derive the client identity
// key used by this invoice with the key path stored in the db.
identityKeyPath := parentPath.Child(identityKeyChildIndex)
nodeHDKey, err := userKey.DeriveTo(identityKeyPath.String())
if err != nil {
return err
}
nodeKey, err := nodeHDKey.key.ECPrivKey()
if err != nil {
return err
}
txInput := tx.TxIn[index]
if txInput.PreviousOutPoint.Hash != htlcTx.TxHash() {
return fmt.Errorf("expected fulfillment tx input to point to htlc tx")
}
if txInput.PreviousOutPoint.Index != uint32(htlcOutputIndex) {
return fmt.Errorf("expected fulfillment tx input to point to correct htlc output")
}
sigHashes := txscript.NewTxSigHashes(tx)
muunSigKey, err := muunPublicKey.key.ECPubKey()
if err != nil {
return err
}
// Verify Muun signature
htlcOutputAmount := htlcTx.TxOut[htlcOutputIndex].Value
err = verifyTxWitnessSignature(
tx,
sigHashes,
index,
htlcOutputAmount,
htlcScript,
c.MuunSignature,
muunSigKey,
)
if err != nil {
return fmt.Errorf("could not verify Muun signature for htlc: %w", err)
}
var outputAmount, expectedAmount lnwire.MilliSatoshi
if c.VerifyOutputAmount {
outputAmount = lnwire.MilliSatoshi(tx.TxOut[0].Value * 1000)
// This incoming swap might be collecting debt, which would be deducted from the outputAmount
// so we add it back up so the amount will match with the sphinx
expectedAmount = outputAmount + lnwire.NewMSatFromSatoshis(c.Collect)
}
// Now check the information we have against the sphinx created by the payer
if len(c.Sphinx) > 0 {
err = sphinx.Validate(
c.Sphinx,
c.PaymentHash256,
secrets.PaymentSecret,
nodeKey,
uint32(c.ExpirationHeight),
expectedAmount,
c.Network,
)
if err != nil {
return fmt.Errorf("could not verify sphinx blob: %w", err)
}
}
// Sign the fulfillment tx
sig, err := signNativeSegwitInput(
index,
tx,
userPrivateKey,
htlcScript,
btcutil.Amount(htlcOutputAmount),
)
if err != nil {
return fmt.Errorf("could not sign fulfillment tx: %w", err)
}
txInput.Witness = wire.TxWitness{
secrets.Preimage,
sig,
c.MuunSignature,
htlcScript,
}
return nil
}
func (c *coinIncomingSwap) FullySignInput(index int, tx *wire.MsgTx, userKey, muunKey *HDPrivateKey) error {
// Lookup invoice data matching this HTLC using the payment hash
db, err := openDB()
if err != nil {
return err
}
defer db.Close()
secrets, err := db.FindByPaymentHash(c.PaymentHash256)
if err != nil {
return fmt.Errorf("could not find invoice data for payment hash: %w", err)
}
derivedMuunKey, err := muunKey.DeriveTo(secrets.KeyPath)
if err != nil {
return fmt.Errorf("failed to derive muun key: %w", err)
}
muunSignature, err := c.signature(index, tx, userKey.PublicKey(), derivedMuunKey.PublicKey(), derivedMuunKey)
if err != nil {
return err
}
c.MuunSignature = muunSignature
return c.SignInput(index, tx, userKey, muunKey.PublicKey())
}
func (c *coinIncomingSwap) createHtlcScript(userPublicKey, muunPublicKey *HDPublicKey) ([]byte, error) {
return createHtlcScript(
userPublicKey.Raw(),
muunPublicKey.Raw(),
c.SwapServerPublicKey,
c.ExpirationHeight,
c.PaymentHash256,
)
}
func (c *coinIncomingSwap) signature(index int, tx *wire.MsgTx, userKey *HDPublicKey, muunKey *HDPublicKey,
signingKey *HDPrivateKey) ([]byte, error) {
htlcTx := wire.MsgTx{}
err := htlcTx.Deserialize(bytes.NewReader(c.HtlcTx))
if err != nil {
return nil, fmt.Errorf("could not deserialize htlc tx: %w", err)
}
htlcScript, err := c.createHtlcScript(userKey, muunKey)
if err != nil {
return nil, fmt.Errorf("could not create htlc script: %w", err)
}
htlcOutputIndex, err := c.findHtlcOutputIndex(&htlcTx, htlcScript)
if err != nil {
return nil, err
}
prevOutAmount := htlcTx.TxOut[htlcOutputIndex].Value
sig, err := signNativeSegwitInput(
index,
tx,
signingKey,
htlcScript,
btcutil.Amount(prevOutAmount),
)
if err != nil {
return nil, fmt.Errorf("could not sign fulfillment tx: %w", err)
}
return sig, nil
}
func (c *coinIncomingSwap) findHtlcOutputIndex(htlcTx *wire.MsgTx, htlcScript []byte) (int, error) {
witnessHash := sha256.Sum256(htlcScript)
address, err := btcutil.NewAddressWitnessScriptHash(witnessHash[:], c.Network)
if err != nil {
return 0, fmt.Errorf("could not create htlc address: %w", err)
}
pkScript, err := txscriptw.PayToAddrScript(address)
if err != nil {
return 0, fmt.Errorf("could not create pk script: %w", err)
}
// Try to find the script we just built inside the HTLC output scripts
for i, out := range htlcTx.TxOut {
if bytes.Equal(pkScript, out.PkScript) {
return i, nil
}
}
return 0, errors.New("could not find valid htlc output in htlc tx")
}
func createHtlcScript(userPublicKey, muunPublicKey, swapServerPublicKey []byte, expiry int64, paymentHash []byte) ([]byte, error) {
sb := txscript.NewScriptBuilder()
sb.AddData(muunPublicKey)
sb.AddOp(txscript.OP_CHECKSIG)
sb.AddOp(txscript.OP_NOTIF)
sb.AddOp(txscript.OP_DUP)
sb.AddOp(txscript.OP_HASH160)
sb.AddData(btcutil.Hash160(swapServerPublicKey))
sb.AddOp(txscript.OP_EQUALVERIFY)
sb.AddOp(txscript.OP_CHECKSIGVERIFY)
sb.AddInt64(expiry)
sb.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
sb.AddOp(txscript.OP_ELSE)
sb.AddData(userPublicKey)
sb.AddOp(txscript.OP_CHECKSIGVERIFY)
sb.AddOp(txscript.OP_SIZE)
sb.AddInt64(32)
sb.AddOp(txscript.OP_EQUALVERIFY)
sb.AddOp(txscript.OP_HASH160)
sb.AddData(ripemd160(paymentHash))
sb.AddOp(txscript.OP_EQUAL)
sb.AddOp(txscript.OP_ENDIF)
return sb.Script()
}