mirror of
https://github.com/muun/recovery.git
synced 2025-11-10 05:59:44 -05:00
Release v0.3.0
This commit is contained in:
107
vendor/github.com/muun/libwallet/swaps/swaps.go
generated
vendored
Normal file
107
vendor/github.com/muun/libwallet/swaps/swaps.go
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package swaps
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/muun/libwallet/addresses"
|
||||
"github.com/muun/libwallet/hdpath"
|
||||
hash "golang.org/x/crypto/ripemd160" //lint:ignore SA1019 using deprecated hash function for compatibility
|
||||
)
|
||||
|
||||
type SubmarineSwap struct {
|
||||
Invoice string
|
||||
Receiver SubmarineSwapReceiver
|
||||
FundingOutput SubmarineSwapFundingOutput
|
||||
PreimageInHex string
|
||||
}
|
||||
|
||||
type SubmarineSwapFundingOutput struct {
|
||||
ScriptVersion int64
|
||||
OutputAddress string
|
||||
OutputAmount int64
|
||||
ConfirmationsNeeded int
|
||||
ServerPaymentHashInHex string
|
||||
ServerPublicKeyInHex string
|
||||
|
||||
UserLockTime int64 // TODO: not checked in v2?
|
||||
|
||||
// v1 only
|
||||
UserRefundAddress *addresses.WalletAddress
|
||||
|
||||
// v2 only
|
||||
ExpirationInBlocks int64
|
||||
UserPublicKey *hdkeychain.ExtendedKey
|
||||
MuunPublicKey *hdkeychain.ExtendedKey
|
||||
KeyPath string
|
||||
}
|
||||
|
||||
type SubmarineSwapReceiver struct {
|
||||
Alias string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
type KeyDescriptor struct {
|
||||
Key *hdkeychain.ExtendedKey
|
||||
Path string
|
||||
}
|
||||
|
||||
func (d *KeyDescriptor) DeriveTo(path string) (*hdkeychain.ExtendedKey, error) {
|
||||
key := d.Key
|
||||
indexes := hdpath.MustParse(path).IndexesFrom(hdpath.MustParse(d.Path))
|
||||
for _, index := range indexes {
|
||||
var err error
|
||||
var modifier uint32
|
||||
if index.Hardened {
|
||||
modifier = hdkeychain.HardenedKeyStart
|
||||
}
|
||||
key, err = key.Child(index.Index | modifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (swap *SubmarineSwap) Validate(
|
||||
rawInvoice string,
|
||||
userPublicKey *KeyDescriptor,
|
||||
muunPublicKey *KeyDescriptor,
|
||||
originalExpirationInBlocks int64,
|
||||
network *chaincfg.Params,
|
||||
) error {
|
||||
|
||||
version := swap.FundingOutput.ScriptVersion
|
||||
switch version {
|
||||
case addresses.SubmarineSwapV1:
|
||||
return swap.validateV1(rawInvoice, userPublicKey, muunPublicKey, network)
|
||||
case addresses.SubmarineSwapV2:
|
||||
return swap.validateV2(rawInvoice, userPublicKey, muunPublicKey, originalExpirationInBlocks, network)
|
||||
default:
|
||||
return fmt.Errorf("unknown swap version %v", version)
|
||||
}
|
||||
}
|
||||
|
||||
func createNonNativeSegwitRedeemScript(witnessScript []byte) ([]byte, error) {
|
||||
witnessScriptHash := sha256.Sum256(witnessScript)
|
||||
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddInt64(0)
|
||||
builder.AddData(witnessScriptHash[:])
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
func ripemd160(data []byte) []byte {
|
||||
hasher := hash.New()
|
||||
_, err := hasher.Write(data)
|
||||
if err != nil {
|
||||
panic("failed to hash")
|
||||
}
|
||||
|
||||
return hasher.Sum([]byte{})
|
||||
}
|
||||
151
vendor/github.com/muun/libwallet/swaps/v1.go
generated
vendored
Normal file
151
vendor/github.com/muun/libwallet/swaps/v1.go
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
package swaps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/muun/libwallet/addresses"
|
||||
)
|
||||
|
||||
func (swap *SubmarineSwap) validateV1(rawInvoice string, userPublicKey, muunPublicKey *KeyDescriptor, network *chaincfg.Params) error {
|
||||
|
||||
invoice, err := zpay32.Decode(rawInvoice, network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode invoice: %w", err)
|
||||
}
|
||||
|
||||
// Check the payment hash matches
|
||||
|
||||
serverPaymentHash, err := hex.DecodeString(swap.FundingOutput.ServerPaymentHashInHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server payment hash is not valid hex: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(invoice.PaymentHash[:], serverPaymentHash) {
|
||||
return fmt.Errorf("payment hash doesn't match %v != %v", hex.EncodeToString(invoice.PaymentHash[:]), swap.FundingOutput.ServerPaymentHashInHex)
|
||||
}
|
||||
|
||||
// TODO: check that timelock is acceptable
|
||||
|
||||
// Validate that the refund address is one we can derive
|
||||
|
||||
swapRefundAddress := swap.FundingOutput.UserRefundAddress
|
||||
derivedUserKey, err := userPublicKey.DeriveTo(swapRefundAddress.DerivationPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive user key: %w", err)
|
||||
}
|
||||
derivedMuunKey, err := muunPublicKey.DeriveTo(swapRefundAddress.DerivationPath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive muun key: %w", err)
|
||||
}
|
||||
|
||||
refundAddress, err := addresses.Create(
|
||||
swapRefundAddress.Version(),
|
||||
derivedUserKey,
|
||||
derivedMuunKey,
|
||||
swapRefundAddress.DerivationPath(),
|
||||
network,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate refund address: %w", err)
|
||||
}
|
||||
|
||||
if refundAddress.Address() != swapRefundAddress.Address() {
|
||||
return fmt.Errorf("refund address doesn't match generated (%v != %v)", swapRefundAddress.Address(), refundAddress.Address())
|
||||
}
|
||||
|
||||
// Check the swap's witness script is a valid swap script
|
||||
|
||||
serverPubKey, err := hex.DecodeString(swap.FundingOutput.ServerPublicKeyInHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server pub key is not hex: %w", err)
|
||||
}
|
||||
|
||||
witnessScript, err := CreateWitnessScriptSubmarineSwapV1(
|
||||
swapRefundAddress.Address(),
|
||||
serverPaymentHash,
|
||||
serverPubKey,
|
||||
swap.FundingOutput.UserLockTime,
|
||||
network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute witness script: %w", err)
|
||||
}
|
||||
|
||||
redeemScript, err := createNonNativeSegwitRedeemScript(witnessScript)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build redeem script: %w", err)
|
||||
}
|
||||
|
||||
address, err := btcutil.NewAddressScriptHash(redeemScript, network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build address for swap script: %w", err)
|
||||
}
|
||||
|
||||
if address.EncodeAddress() != swap.FundingOutput.OutputAddress {
|
||||
return fmt.Errorf("address for swap script mismatch (%v != %v)", address.EncodeAddress(), swap.FundingOutput.OutputAddress)
|
||||
}
|
||||
|
||||
if len(swap.PreimageInHex) > 0 {
|
||||
preimage, err := hex.DecodeString(swap.PreimageInHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("preimagehex is not actually hex: %w", err)
|
||||
}
|
||||
|
||||
calculatedPaymentHash := sha256.Sum256(preimage)
|
||||
if !bytes.Equal(invoice.PaymentHash[:], calculatedPaymentHash[:]) {
|
||||
return fmt.Errorf("payment hash doesn't match preimage (%v != hash(%v)", invoice.PaymentHash, swap.PreimageInHex)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateWitnessScriptSubmarineSwapV1(refundAddress string, paymentHash []byte, swapServerPubKey []byte, lockTime int64, network *chaincfg.Params) ([]byte, error) {
|
||||
|
||||
// It turns out that the payment hash present in an invoice is just the SHA256 of the
|
||||
// payment preimage, so we still have to do a pass of RIPEMD160 before pushing it to the
|
||||
// script
|
||||
paymentHash160 := ripemd160(paymentHash)
|
||||
decodedRefundAddress, err := btcutil.DecodeAddress(refundAddress, network)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("refund address is invalid: %w", err)
|
||||
}
|
||||
|
||||
refundAddressHash := decodedRefundAddress.ScriptAddress()
|
||||
|
||||
builder := txscript.NewScriptBuilder()
|
||||
builder.AddOp(txscript.OP_DUP)
|
||||
|
||||
// Condition to decide which branch to follow:
|
||||
builder.AddOp(txscript.OP_HASH160).
|
||||
AddData(paymentHash160).
|
||||
AddOp(txscript.OP_EQUAL)
|
||||
|
||||
// SubmarineSwap service spending script, for successful LN payments:
|
||||
builder.AddOp(txscript.OP_IF).
|
||||
AddOp(txscript.OP_DROP).
|
||||
AddData(swapServerPubKey)
|
||||
|
||||
// User spending script, for failed LN payments:
|
||||
builder.AddOp(txscript.OP_ELSE).
|
||||
AddInt64(lockTime).
|
||||
AddOp(txscript.OP_CHECKLOCKTIMEVERIFY).
|
||||
AddOp(txscript.OP_DROP).
|
||||
AddOp(txscript.OP_DUP).
|
||||
AddOp(txscript.OP_HASH160).
|
||||
AddData(refundAddressHash).
|
||||
AddOp(txscript.OP_EQUALVERIFY)
|
||||
|
||||
// Final verification for both branches:
|
||||
builder.AddOp(txscript.OP_ENDIF).
|
||||
AddOp(txscript.OP_CHECKSIG)
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
205
vendor/github.com/muun/libwallet/swaps/v2.go
generated
vendored
Normal file
205
vendor/github.com/muun/libwallet/swaps/v2.go
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
package swaps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/btcsuite/btcutil/hdkeychain"
|
||||
"github.com/lightningnetwork/lnd/zpay32"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (swap *SubmarineSwap) validateV2(rawInvoice string, userPublicKey, muunPublicKey *KeyDescriptor, originalExpirationInBlocks int64, network *chaincfg.Params) error {
|
||||
|
||||
fundingOutput := swap.FundingOutput
|
||||
|
||||
invoice, err := zpay32.Decode(rawInvoice, network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode invoice: %w", err)
|
||||
}
|
||||
|
||||
// Check the payment hash matches
|
||||
|
||||
serverPaymentHash, err := hex.DecodeString(fundingOutput.ServerPaymentHashInHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server payment hash is not valid hex: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(invoice.PaymentHash[:], serverPaymentHash) {
|
||||
return fmt.Errorf("payment hash doesn't match %v != %v", hex.EncodeToString(invoice.PaymentHash[:]), fundingOutput.ServerPaymentHashInHex)
|
||||
}
|
||||
|
||||
destination, err := hex.DecodeString(swap.Receiver.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("destination is not valid hex: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(invoice.Destination.SerializeCompressed(), destination) {
|
||||
return fmt.Errorf("destination doesnt match %v != %v", invoice.Destination.SerializeCompressed(), swap.Receiver.PublicKey)
|
||||
}
|
||||
|
||||
if fundingOutput.ExpirationInBlocks != originalExpirationInBlocks {
|
||||
return fmt.Errorf("expiration in blocks doesnt match %v != %v", originalExpirationInBlocks, fundingOutput.ExpirationInBlocks)
|
||||
}
|
||||
|
||||
// Validate that we can derive the addresses involved
|
||||
derivationPath := fundingOutput.KeyPath
|
||||
|
||||
derivedUserKey, err := userPublicKey.DeriveTo(derivationPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive user key: %w", err)
|
||||
}
|
||||
|
||||
if derivedUserKey.String() != fundingOutput.UserPublicKey.String() {
|
||||
return fmt.Errorf("user pub keys dont match %v != %v", derivedUserKey.String(), fundingOutput.UserPublicKey.String())
|
||||
}
|
||||
|
||||
derivedMuunKey, err := muunPublicKey.DeriveTo(derivationPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive muun key: %w", err)
|
||||
}
|
||||
|
||||
if derivedMuunKey.String() != fundingOutput.MuunPublicKey.String() {
|
||||
return fmt.Errorf("muun pub keys dont match %v != %v", derivedMuunKey.String(), fundingOutput.MuunPublicKey.String())
|
||||
}
|
||||
|
||||
// Check the swap's witness script is a valid swap script
|
||||
|
||||
serverPubKey, err := hex.DecodeString(swap.FundingOutput.ServerPublicKeyInHex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server pub key is not hex: %w", err)
|
||||
}
|
||||
|
||||
witnessScript, err := CreateWitnessScriptSubmarineSwapV2(
|
||||
serverPaymentHash,
|
||||
encodeRaw(derivedUserKey),
|
||||
encodeRaw(derivedMuunKey),
|
||||
serverPubKey,
|
||||
swap.FundingOutput.ExpirationInBlocks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute witness script: %w", err)
|
||||
}
|
||||
|
||||
witnessScriptHash := sha256.Sum256(witnessScript)
|
||||
address, err := btcutil.NewAddressWitnessScriptHash(witnessScriptHash[:], network)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build address for swap script: %w", err)
|
||||
}
|
||||
|
||||
if address.EncodeAddress() != swap.FundingOutput.OutputAddress {
|
||||
return fmt.Errorf("address for swap script mismatch (%v != %v)", address.EncodeAddress(), swap.FundingOutput.OutputAddress)
|
||||
}
|
||||
|
||||
if len(swap.PreimageInHex) > 0 {
|
||||
preimage, err := hex.DecodeString(swap.PreimageInHex)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "preimagehex is not actually hex 🤔")
|
||||
}
|
||||
|
||||
calculatedPaymentHash := sha256.Sum256(preimage)
|
||||
if !bytes.Equal(invoice.PaymentHash[:], calculatedPaymentHash[:]) {
|
||||
return fmt.Errorf("payment hash doesn't match preimage (%v != hash(%v)", invoice.PaymentHash, swap.PreimageInHex)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateWitnessScriptSubmarineSwapV2(paymentHash, userPubKey, muunPubKey, swapServerPubKey []byte, blocksForExpiration int64) ([]byte, error) {
|
||||
|
||||
// It turns out that the payment hash present in an invoice is just the SHA256 of the
|
||||
// payment preimage, so we still have to do a pass of RIPEMD160 before pushing it to the
|
||||
// script
|
||||
paymentHash160 := ripemd160(paymentHash)
|
||||
muunPublicKeyHash160 := btcutil.Hash160(muunPubKey)
|
||||
|
||||
// Equivalent miniscript (http://bitcoin.sipa.be/miniscript/):
|
||||
// or(
|
||||
// and(pk(userPublicKey), pk(swapServerPublicKey)),
|
||||
// or(
|
||||
// and(pk(swapServerPublicKey), hash160(swapPaymentHash160)),
|
||||
// and(pk(userPublicKey), and(pk(muunPublicKey), older(numBlocksForExpiration)))
|
||||
// )
|
||||
// )
|
||||
//
|
||||
// However, we differ in that the size of the script was heavily optimized for spending the
|
||||
// first two branches (the collaborative close and the unilateral close by swapper), which
|
||||
// are the most probable to be used.
|
||||
|
||||
builder := txscript.NewScriptBuilder().
|
||||
// Push the user public key to the second position of the stack
|
||||
AddData(userPubKey).
|
||||
AddOp(txscript.OP_SWAP).
|
||||
|
||||
// Check whether the first stack item was a valid swap server signature
|
||||
AddData(swapServerPubKey).
|
||||
AddOp(txscript.OP_CHECKSIG).
|
||||
|
||||
// If the swap server signature was correct
|
||||
AddOp(txscript.OP_IF).
|
||||
AddOp(txscript.OP_SWAP).
|
||||
|
||||
// Check whether the second stack item was the payment preimage
|
||||
AddOp(txscript.OP_DUP).
|
||||
AddOp(txscript.OP_HASH160).
|
||||
AddData(paymentHash160).
|
||||
AddOp(txscript.OP_EQUAL).
|
||||
|
||||
// If the preimage was correct
|
||||
AddOp(txscript.OP_IF).
|
||||
// We are done, leave just one true-ish item in the stack (there're 2
|
||||
// remaining items)
|
||||
AddOp(txscript.OP_DROP).
|
||||
|
||||
// If the second stack item wasn't a valid payment preimage
|
||||
AddOp(txscript.OP_ELSE).
|
||||
|
||||
// Validate that the second stack item was a valid user signature
|
||||
AddOp(txscript.OP_SWAP).
|
||||
AddOp(txscript.OP_CHECKSIG).
|
||||
AddOp(txscript.OP_ENDIF).
|
||||
|
||||
// If the first stack item wasn't a valid server signature
|
||||
AddOp(txscript.OP_ELSE).
|
||||
// Validate that the blockchain height is big enough
|
||||
AddInt64(blocksForExpiration).
|
||||
AddOp(txscript.OP_CHECKSEQUENCEVERIFY).
|
||||
AddOp(txscript.OP_DROP).
|
||||
|
||||
// Validate that the second stack item was a valid user signature
|
||||
AddOp(txscript.OP_CHECKSIGVERIFY).
|
||||
|
||||
// Validate that the third stack item was the muun public key
|
||||
AddOp(txscript.OP_DUP).
|
||||
AddOp(txscript.OP_HASH160).
|
||||
AddData(muunPublicKeyHash160).
|
||||
AddOp(txscript.OP_EQUALVERIFY).
|
||||
|
||||
// Notice that instead of directly pushing the public key here and checking the
|
||||
// signature P2PK-style, we pushed the hash of the public key, and require an
|
||||
// extra stack item with the actual public key, verifying the signature and
|
||||
// public key P2PKH-style.
|
||||
//
|
||||
// This trick reduces the on-chain footprint of the muun key from 33 bytes to
|
||||
// 20 bytes for the collaborative, and swap server's non-collaborative branches,
|
||||
// which are the most frequent ones.
|
||||
|
||||
// Validate that the fourth stack item was a valid server signature
|
||||
AddOp(txscript.OP_CHECKSIG).
|
||||
AddOp(txscript.OP_ENDIF)
|
||||
|
||||
return builder.Script()
|
||||
}
|
||||
|
||||
func encodeRaw(key *hdkeychain.ExtendedKey) []byte {
|
||||
publicKey, err := key.ECPubKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return publicKey.SerializeCompressed()
|
||||
}
|
||||
Reference in New Issue
Block a user