2020-11-09 10:05:29 -03:00
|
|
|
package libwallet
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/hex"
|
2021-01-29 18:51:08 -03:00
|
|
|
"errors"
|
2020-11-09 10:05:29 -03:00
|
|
|
"fmt"
|
|
|
|
"path"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec"
|
|
|
|
"github.com/btcsuite/btcd/txscript"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
|
|
"github.com/lightningnetwork/lnd/netann"
|
|
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
|
|
|
|
|
|
"github.com/muun/libwallet/hdpath"
|
2021-01-29 18:51:08 -03:00
|
|
|
"github.com/muun/libwallet/sphinx"
|
2020-11-09 10:05:29 -03:00
|
|
|
"github.com/muun/libwallet/walletdb"
|
|
|
|
)
|
|
|
|
|
|
|
|
const MaxUnusedSecrets = 5
|
|
|
|
|
|
|
|
const (
|
|
|
|
identityKeyChildIndex = 0
|
|
|
|
htlcKeyChildIndex = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
// InvoiceSecrets represents a bundle of secrets required to generate invoices
|
|
|
|
// from the client. These secrets must be registered with the remote server
|
|
|
|
// and persisted in the client database before use.
|
|
|
|
type InvoiceSecrets struct {
|
|
|
|
preimage []byte
|
|
|
|
paymentSecret []byte
|
|
|
|
keyPath string
|
|
|
|
PaymentHash []byte
|
|
|
|
IdentityKey *HDPublicKey
|
|
|
|
UserHtlcKey *HDPublicKey
|
|
|
|
MuunHtlcKey *HDPublicKey
|
|
|
|
ShortChanId int64
|
|
|
|
}
|
|
|
|
|
|
|
|
// RouteHints is a struct returned by the remote server containing the data
|
|
|
|
// necessary for constructing an invoice locally.
|
|
|
|
type RouteHints struct {
|
|
|
|
Pubkey string
|
|
|
|
FeeBaseMsat int64
|
|
|
|
FeeProportionalMillionths int64
|
|
|
|
CltvExpiryDelta int32
|
|
|
|
}
|
|
|
|
|
|
|
|
// InvoiceOptions defines additional options that can be configured when
|
|
|
|
// creating a new invoice.
|
|
|
|
type InvoiceOptions struct {
|
|
|
|
Description string
|
|
|
|
AmountSat int64
|
|
|
|
}
|
|
|
|
|
|
|
|
// InvoiceSecretsList is a wrapper around an InvoiceSecrets slice to be
|
|
|
|
// able to pass through the gomobile bridge.
|
|
|
|
type InvoiceSecretsList struct {
|
|
|
|
secrets []*InvoiceSecrets
|
|
|
|
}
|
|
|
|
|
|
|
|
// Length returns the number of secrets in the list.
|
|
|
|
func (l *InvoiceSecretsList) Length() int {
|
|
|
|
return len(l.secrets)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get returns the secret at the given index.
|
|
|
|
func (l *InvoiceSecretsList) Get(i int) *InvoiceSecrets {
|
|
|
|
return l.secrets[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateInvoiceSecrets returns a slice of new secrets to register with
|
|
|
|
// the remote server. Once registered, those invoices should be stored with
|
|
|
|
// the PersistInvoiceSecrets method.
|
|
|
|
func GenerateInvoiceSecrets(userKey, muunKey *HDPublicKey) (*InvoiceSecretsList, error) {
|
|
|
|
|
|
|
|
var secrets []*InvoiceSecrets
|
|
|
|
|
|
|
|
db, err := openDB()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
unused, err := db.CountUnusedInvoices()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if unused >= MaxUnusedSecrets {
|
|
|
|
return &InvoiceSecretsList{make([]*InvoiceSecrets, 0)}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
num := MaxUnusedSecrets - unused
|
|
|
|
|
|
|
|
for i := 0; i < num; i++ {
|
|
|
|
preimage := randomBytes(32)
|
|
|
|
paymentSecret := randomBytes(32)
|
|
|
|
paymentHashArray := sha256.Sum256(preimage)
|
|
|
|
paymentHash := paymentHashArray[:]
|
|
|
|
|
|
|
|
levels := randomBytes(8)
|
|
|
|
l1 := binary.LittleEndian.Uint32(levels[:4]) & 0x7FFFFFFF
|
|
|
|
l2 := binary.LittleEndian.Uint32(levels[4:]) & 0x7FFFFFFF
|
|
|
|
|
|
|
|
keyPath := hdpath.MustParse("m/schema:1'/recovery:1'/invoices:4").Child(l1).Child(l2)
|
|
|
|
|
|
|
|
identityKeyPath := keyPath.Child(identityKeyChildIndex)
|
|
|
|
|
|
|
|
identityKey, err := userKey.DeriveTo(identityKeyPath.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
htlcKeyPath := keyPath.Child(htlcKeyChildIndex)
|
|
|
|
|
|
|
|
userHtlcKey, err := userKey.DeriveTo(htlcKeyPath.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
muunHtlcKey, err := muunKey.DeriveTo(htlcKeyPath.String())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
shortChanId := binary.LittleEndian.Uint64(randomBytes(8)) | (1 << 63)
|
|
|
|
|
|
|
|
secrets = append(secrets, &InvoiceSecrets{
|
|
|
|
preimage: preimage,
|
|
|
|
paymentSecret: paymentSecret,
|
|
|
|
keyPath: keyPath.String(),
|
|
|
|
PaymentHash: paymentHash,
|
|
|
|
IdentityKey: identityKey,
|
|
|
|
UserHtlcKey: userHtlcKey,
|
|
|
|
MuunHtlcKey: muunHtlcKey,
|
|
|
|
ShortChanId: int64(shortChanId),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: cleanup used secrets
|
|
|
|
|
|
|
|
return &InvoiceSecretsList{secrets}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PersistInvoiceSecrets stores secrets registered with the remote server
|
|
|
|
// in the device local database. These secrets can be used to craft new
|
|
|
|
// Lightning invoices.
|
|
|
|
func PersistInvoiceSecrets(list *InvoiceSecretsList) error {
|
|
|
|
db, err := openDB()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
for _, s := range list.secrets {
|
|
|
|
db.CreateInvoice(&walletdb.Invoice{
|
|
|
|
Preimage: s.preimage,
|
|
|
|
PaymentHash: s.PaymentHash,
|
|
|
|
PaymentSecret: s.paymentSecret,
|
|
|
|
KeyPath: s.keyPath,
|
|
|
|
ShortChanId: uint64(s.ShortChanId),
|
|
|
|
State: walletdb.InvoiceStateRegistered,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateInvoice returns a new lightning invoice string for the given network.
|
|
|
|
// Amount and description can be configured optionally.
|
|
|
|
func CreateInvoice(net *Network, userKey *HDPrivateKey, routeHints *RouteHints, opts *InvoiceOptions) (string, error) {
|
|
|
|
// obtain first unused secret from db
|
|
|
|
db, err := openDB()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
defer db.Close()
|
|
|
|
|
|
|
|
dbInvoice, err := db.FindFirstUnusedInvoice()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2021-01-29 18:51:08 -03:00
|
|
|
if dbInvoice == nil {
|
|
|
|
return "", nil
|
|
|
|
}
|
2020-11-09 10:05:29 -03:00
|
|
|
|
|
|
|
var paymentHash [32]byte
|
|
|
|
copy(paymentHash[:], dbInvoice.PaymentHash)
|
|
|
|
|
|
|
|
nodeID, err := parsePubKey(routeHints.Pubkey)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("can't parse route hint pubkey: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var iopts []func(*zpay32.Invoice)
|
|
|
|
iopts = append(iopts, zpay32.RouteHint([]zpay32.HopHint{
|
|
|
|
zpay32.HopHint{
|
|
|
|
NodeID: nodeID,
|
|
|
|
ChannelID: dbInvoice.ShortChanId,
|
|
|
|
FeeBaseMSat: uint32(routeHints.FeeBaseMsat),
|
|
|
|
FeeProportionalMillionths: uint32(routeHints.FeeProportionalMillionths),
|
|
|
|
CLTVExpiryDelta: uint16(routeHints.CltvExpiryDelta),
|
|
|
|
},
|
|
|
|
}))
|
|
|
|
|
|
|
|
features := lnwire.EmptyFeatureVector()
|
|
|
|
features.RawFeatureVector.Set(lnwire.TLVOnionPayloadOptional)
|
|
|
|
features.RawFeatureVector.Set(lnwire.PaymentAddrOptional)
|
|
|
|
|
|
|
|
iopts = append(iopts, zpay32.Features(features))
|
|
|
|
iopts = append(iopts, zpay32.CLTVExpiry(144)) // ~1 day
|
|
|
|
iopts = append(iopts, zpay32.Expiry(1*time.Hour))
|
|
|
|
|
|
|
|
var paymentAddr [32]byte
|
|
|
|
copy(paymentAddr[:], dbInvoice.PaymentSecret)
|
|
|
|
iopts = append(iopts, zpay32.PaymentAddr(paymentAddr))
|
|
|
|
|
|
|
|
if opts.Description != "" {
|
|
|
|
iopts = append(iopts, zpay32.Description(opts.Description))
|
|
|
|
} else {
|
|
|
|
// description or description hash must be non-empty, adding a placeholder for now
|
|
|
|
iopts = append(iopts, zpay32.Description(""))
|
|
|
|
}
|
|
|
|
if opts.AmountSat != 0 {
|
|
|
|
msat := lnwire.NewMSatFromSatoshis(btcutil.Amount(opts.AmountSat))
|
|
|
|
iopts = append(iopts, zpay32.Amount(msat))
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the invoice
|
|
|
|
invoice, err := zpay32.NewInvoice(
|
|
|
|
net.network, paymentHash, time.Now(), iopts...,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
// recreate the client identity privkey
|
|
|
|
identityKeyPath := hdpath.MustParse(dbInvoice.KeyPath).Child(identityKeyChildIndex)
|
|
|
|
identityHDKey, err := userKey.DeriveTo(identityKeyPath.String())
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
identityKey, err := identityHDKey.key.ECPrivKey()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("can't obtain identity privkey: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// sign the invoice with the identity pubkey
|
|
|
|
signer := netann.NewNodeSigner(identityKey)
|
|
|
|
bech32, err := invoice.Encode(zpay32.MessageSigner{
|
|
|
|
SignCompact: signer.SignDigestCompact,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
dbInvoice.State = walletdb.InvoiceStateUsed
|
|
|
|
dbInvoice.UsedAt = &now
|
|
|
|
|
|
|
|
err = db.SaveInvoice(dbInvoice)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return bech32, nil
|
|
|
|
}
|
|
|
|
|
2021-01-29 18:51:08 -03:00
|
|
|
// ExposePreimage gives the preimage matching a payment hash if we have it
|
|
|
|
func ExposePreimage(paymentHash []byte) ([]byte, error) {
|
|
|
|
|
|
|
|
if len(paymentHash) != 32 {
|
|
|
|
return nil, fmt.Errorf("ExposePreimage: received invalid hash len %v", len(paymentHash))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(paymentHash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not find invoice data for payment hash: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return secrets.Preimage, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func IsInvoiceFulfillable(paymentHash, onionBlob []byte, amount int64, userKey *HDPrivateKey, net *Network) error {
|
|
|
|
if len(paymentHash) != 32 {
|
|
|
|
return fmt.Errorf("IsInvoiceFulfillable: received invalid hash len %v", len(paymentHash))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(paymentHash)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("IsInvoiceFulfillable: could not find invoice data for payment hash: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(onionBlob) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
identityKeyPath := hdpath.MustParse(secrets.KeyPath).Child(identityKeyChildIndex)
|
|
|
|
|
|
|
|
nodeHDKey, err := userKey.DeriveTo(identityKeyPath.String())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("IsInvoiceFulfillable: failed to derive key: %w", err)
|
|
|
|
}
|
|
|
|
nodeKey, err := nodeHDKey.key.ECPrivKey()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("IsInvoiceFulfillable: failed to get priv key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = sphinx.Validate(
|
|
|
|
onionBlob,
|
|
|
|
paymentHash,
|
|
|
|
secrets.PaymentSecret,
|
|
|
|
nodeKey,
|
|
|
|
0, // This is used internally by the sphinx decoder but it's not needed
|
|
|
|
lnwire.MilliSatoshi(uint64(amount)*1000),
|
|
|
|
net.network,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("IsInvoiceFuflillable: invalid sphinx: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-11-09 10:05:29 -03:00
|
|
|
type IncomingSwap struct {
|
|
|
|
FulfillmentTx []byte
|
|
|
|
MuunSignature []byte
|
|
|
|
Sphinx []byte
|
|
|
|
PaymentHash []byte
|
|
|
|
BlockHeight int64 // unused
|
|
|
|
HtlcTx []byte
|
|
|
|
OutputVersion int // unused
|
|
|
|
OutputPath string // unused
|
|
|
|
SwapServerPublicKey string
|
|
|
|
MerkleTree []byte // unused
|
|
|
|
HtlcExpiration int64
|
|
|
|
HtlcBlock []byte // unused
|
|
|
|
ConfirmationTarget int64 // to validate fee rate, unused for now
|
2021-01-29 18:51:08 -03:00
|
|
|
CollectInSats int64
|
2020-11-09 10:05:29 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *IncomingSwap) VerifyAndFulfill(userKey *HDPrivateKey, muunKey *HDPublicKey, net *Network) ([]byte, error) {
|
|
|
|
// Validate the fullfillment tx proposed by Muun.
|
|
|
|
tx := wire.MsgTx{}
|
|
|
|
err := tx.DeserializeNoWitness(bytes.NewReader(s.FulfillmentTx))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not deserialize fulfillment tx: %w", err)
|
|
|
|
}
|
|
|
|
if len(tx.TxIn) != 1 {
|
|
|
|
return nil, fmt.Errorf("expected fulfillment tx to have exactly 1 input, found %d", len(tx.TxIn))
|
|
|
|
}
|
|
|
|
if len(tx.TxOut) != 1 {
|
|
|
|
return nil, fmt.Errorf("expected fulfillment tx to have exactly 1 output, found %d", len(tx.TxOut))
|
|
|
|
}
|
|
|
|
|
|
|
|
swapServerPublicKey, err := hex.DecodeString(s.SwapServerPublicKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sign the htlc input (there is only one, at index 0)
|
|
|
|
coin := coinIncomingSwap{
|
|
|
|
Network: net.network,
|
|
|
|
MuunSignature: s.MuunSignature,
|
|
|
|
Sphinx: s.Sphinx,
|
|
|
|
HtlcTx: s.HtlcTx,
|
|
|
|
PaymentHash256: s.PaymentHash,
|
|
|
|
SwapServerPublicKey: swapServerPublicKey,
|
|
|
|
ExpirationHeight: s.HtlcExpiration,
|
|
|
|
VerifyOutputAmount: true,
|
2021-01-29 18:51:08 -03:00
|
|
|
Collect: btcutil.Amount(s.CollectInSats),
|
2020-11-09 10:05:29 -03:00
|
|
|
}
|
|
|
|
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("could not serialize fulfillment tx: %w", err)
|
|
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func openDB() (*walletdb.DB, error) {
|
|
|
|
return walletdb.Open(path.Join(cfg.DataDir, "wallet.db"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func parsePubKey(s string) (*btcec.PublicKey, error) {
|
|
|
|
bytes, err := hex.DecodeString(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return btcec.ParsePubKey(bytes, btcec.S256())
|
|
|
|
}
|
|
|
|
|
|
|
|
func verifyTxWitnessSignature(tx *wire.MsgTx, sigHashes *txscript.TxSigHashes, outputIndex int, amount int64, script []byte, sig []byte, signKey *btcec.PublicKey) error {
|
|
|
|
sigHash, err := txscript.CalcWitnessSigHash(script, sigHashes, txscript.SigHashAll, tx, outputIndex, amount)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
signature, err := btcec.ParseDERSignature(sig, btcec.S256())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !signature.Verify(sigHash, signKey) {
|
|
|
|
return errors.New("signature does not verify")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|