mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 14:40:30 +01:00
input+signer: test wallet script import
This commit is contained in:
parent
c73cf03a55
commit
108f49f23b
4 changed files with 197 additions and 7 deletions
|
@ -3,8 +3,11 @@ package input
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
|
secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will
|
// NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will
|
||||||
|
@ -42,3 +45,53 @@ func MultiPrevOutFetcher(inputs []Input) (*txscript.MultiPrevOutFetcher, error)
|
||||||
|
|
||||||
return fetcher, nil
|
return fetcher, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TapscriptFullTree creates a waddrmgr.Tapscript for the given internal key and
|
||||||
|
// tree leaves.
|
||||||
|
func TapscriptFullTree(internalKey *btcec.PublicKey,
|
||||||
|
allTreeLeaves ...txscript.TapLeaf) *waddrmgr.Tapscript {
|
||||||
|
|
||||||
|
tree := txscript.AssembleTaprootScriptTree(allTreeLeaves...)
|
||||||
|
rootHash := tree.RootNode.TapHash()
|
||||||
|
tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:])
|
||||||
|
|
||||||
|
var outputKeyYIsOdd bool
|
||||||
|
if tapKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd {
|
||||||
|
outputKeyYIsOdd = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &waddrmgr.Tapscript{
|
||||||
|
Type: waddrmgr.TapscriptTypeFullTree,
|
||||||
|
ControlBlock: &txscript.ControlBlock{
|
||||||
|
InternalKey: internalKey,
|
||||||
|
OutputKeyYIsOdd: outputKeyYIsOdd,
|
||||||
|
LeafVersion: txscript.BaseLeafVersion,
|
||||||
|
},
|
||||||
|
Leaves: allTreeLeaves,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TapscriptPartialReveal creates a waddrmgr.Tapscript for the given internal
|
||||||
|
// key and revealed script.
|
||||||
|
func TapscriptPartialReveal(internalKey *btcec.PublicKey,
|
||||||
|
revealedLeaf txscript.TapLeaf,
|
||||||
|
inclusionProof [32]byte) *waddrmgr.Tapscript {
|
||||||
|
|
||||||
|
controlBlock := &txscript.ControlBlock{
|
||||||
|
InternalKey: internalKey,
|
||||||
|
LeafVersion: txscript.BaseLeafVersion,
|
||||||
|
InclusionProof: inclusionProof[:],
|
||||||
|
}
|
||||||
|
rootHash := controlBlock.RootHash(revealedLeaf.Script)
|
||||||
|
tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
|
||||||
|
|
||||||
|
if tapKey.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd {
|
||||||
|
controlBlock.OutputKeyYIsOdd = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &waddrmgr.Tapscript{
|
||||||
|
Type: waddrmgr.TapscriptTypePartialReveal,
|
||||||
|
ControlBlock: controlBlock,
|
||||||
|
RevealedScript: revealedLeaf.Script,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -750,6 +750,26 @@ func (b *BtcWallet) ImportPublicKey(pubKey *btcec.PublicKey,
|
||||||
return b.wallet.ImportPublicKey(pubKey, addrType)
|
return b.wallet.ImportPublicKey(pubKey, addrType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImportTaprootScript imports a user-provided taproot script into the address
|
||||||
|
// manager. The imported script will act as a pay-to-taproot address.
|
||||||
|
func (b *BtcWallet) ImportTaprootScript(scope waddrmgr.KeyScope,
|
||||||
|
tapscript *waddrmgr.Tapscript) (waddrmgr.ManagedAddress, error) {
|
||||||
|
|
||||||
|
// We want to be able to import script addresses into a watch-only
|
||||||
|
// wallet, which is only possible if we don't encrypt the script with
|
||||||
|
// the private key encryption key. By specifying the script as being
|
||||||
|
// "not secret", we can also decrypt the script in a watch-only wallet.
|
||||||
|
const isSecretScript = false
|
||||||
|
|
||||||
|
// Currently, only v1 (Taproot) scripts are supported. We don't even
|
||||||
|
// know what a v2 witness version would look like at this point.
|
||||||
|
const witnessVersionTaproot byte = 1
|
||||||
|
|
||||||
|
return b.wallet.ImportTaprootScript(
|
||||||
|
scope, tapscript, nil, witnessVersionTaproot, isSecretScript,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to
|
// SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to
|
||||||
// the specified outputs. In the case the wallet has insufficient funds, or the
|
// the specified outputs. In the case the wallet has insufficient funds, or the
|
||||||
// outputs are non-standard, a non-nil error will be returned.
|
// outputs are non-standard, a non-nil error will be returned.
|
||||||
|
|
|
@ -250,7 +250,7 @@ func serializeTxWitness(txWitness wire.TxWitness) ([]byte, error) {
|
||||||
|
|
||||||
// TestSignPsbt tests the PSBT signing functionality.
|
// TestSignPsbt tests the PSBT signing functionality.
|
||||||
func TestSignPsbt(t *testing.T) {
|
func TestSignPsbt(t *testing.T) {
|
||||||
w, cleanup := newTestWallet(t, netParams, seedBytes)
|
w, _, cleanup := newTestWallet(t, netParams, seedBytes)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
|
|
|
@ -2,17 +2,25 @@ package btcwallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/integration/rpctest"
|
"github.com/btcsuite/btcd/integration/rpctest"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcwallet/chain"
|
"github.com/btcsuite/btcwallet/chain"
|
||||||
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/lightningnetwork/lnd/blockcache"
|
"github.com/lightningnetwork/lnd/blockcache"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -32,6 +40,29 @@ var (
|
||||||
// which is a special case for the BIP49/84 addresses in btcwallet).
|
// which is a special case for the BIP49/84 addresses in btcwallet).
|
||||||
firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p"
|
firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p"
|
||||||
|
|
||||||
|
// firstAddressTaproot is the first address that we should get from the
|
||||||
|
// wallet when deriving a taproot address.
|
||||||
|
firstAddressTaproot = "bcrt1ps8c222fgysvnsj2m8hxk8khy6wthcrhv9va9z3t4" +
|
||||||
|
"h3qeyz65sh4qqwvdgc"
|
||||||
|
|
||||||
|
testPubKeyBytes, _ = hex.DecodeString(
|
||||||
|
"037a67771635344641d4b56aac33cd5f7a265b59678dce3aec31b89125e3" +
|
||||||
|
"b8b9b2",
|
||||||
|
)
|
||||||
|
testPubKey, _ = btcec.ParsePubKey(testPubKeyBytes)
|
||||||
|
testTaprootKeyBytes, _ = hex.DecodeString(
|
||||||
|
"03f068684c9141027318eed958dccbf4f7f748700e1da53315630d82a362" +
|
||||||
|
"d6a887",
|
||||||
|
)
|
||||||
|
testTaprootKey, _ = btcec.ParsePubKey(testTaprootKeyBytes)
|
||||||
|
|
||||||
|
testTapscriptAddr = "bcrt1p7p5xsny3gyp8xx8wm9vdejl57lm5suqwrkjnx9trpk" +
|
||||||
|
"p2xckk4zrs4xehl8"
|
||||||
|
testTapscriptPkScript = append(
|
||||||
|
[]byte{txscript.OP_1, txscript.OP_DATA_32},
|
||||||
|
schnorr.SerializePubKey(testTaprootKey)...,
|
||||||
|
)
|
||||||
|
|
||||||
testCases = []struct {
|
testCases = []struct {
|
||||||
name string
|
name string
|
||||||
path []uint32
|
path []uint32
|
||||||
|
@ -133,7 +164,7 @@ var (
|
||||||
// BIP32 key path correctly.
|
// BIP32 key path correctly.
|
||||||
func TestBip32KeyDerivation(t *testing.T) {
|
func TestBip32KeyDerivation(t *testing.T) {
|
||||||
netParams := &chaincfg.RegressionNetParams
|
netParams := &chaincfg.RegressionNetParams
|
||||||
w, cleanup := newTestWallet(t, netParams, seedBytes)
|
w, _, cleanup := newTestWallet(t, netParams, seedBytes)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// This is just a sanity check that the wallet was initialized
|
// This is just a sanity check that the wallet was initialized
|
||||||
|
@ -167,8 +198,94 @@ func TestBip32KeyDerivation(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestScriptImport tests the btcwallet's tapscript import capabilities by
|
||||||
|
// importing both a full taproot script tree and a partially revealed branch
|
||||||
|
// with a proof to make sure the resulting addresses match up.
|
||||||
|
func TestScriptImport(t *testing.T) {
|
||||||
|
netParams := &chaincfg.RegressionNetParams
|
||||||
|
w, miner, cleanup := newTestWallet(t, netParams, seedBytes)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
firstDerivedAddr, err := w.NewAddress(
|
||||||
|
lnwallet.TaprootPubkey, false, lnwallet.DefaultAccountName,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, firstAddressTaproot, firstDerivedAddr.String())
|
||||||
|
|
||||||
|
scope := waddrmgr.KeyScopeBIP0086
|
||||||
|
_, err = w.InternalWallet().Manager.FetchScopedKeyManager(scope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Let's create a taproot script output now. This is a hash lock with a
|
||||||
|
// simple preimage of "foobar".
|
||||||
|
builder := txscript.NewScriptBuilder()
|
||||||
|
builder.AddOp(txscript.OP_DUP)
|
||||||
|
builder.AddOp(txscript.OP_HASH160)
|
||||||
|
builder.AddData(btcutil.Hash160([]byte("foobar")))
|
||||||
|
builder.AddOp(txscript.OP_EQUALVERIFY)
|
||||||
|
script1, err := builder.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf1 := txscript.NewBaseTapLeaf(script1)
|
||||||
|
|
||||||
|
// Let's add a second script output as well to test the partial reveal.
|
||||||
|
builder = txscript.NewScriptBuilder()
|
||||||
|
builder.AddData(schnorr.SerializePubKey(testPubKey))
|
||||||
|
builder.AddOp(txscript.OP_CHECKSIG)
|
||||||
|
script2, err := builder.Script()
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf2 := txscript.NewBaseTapLeaf(script2)
|
||||||
|
|
||||||
|
// Our first test case is storing the script with all its leaves.
|
||||||
|
tapscript1 := input.TapscriptFullTree(testPubKey, leaf1, leaf2)
|
||||||
|
|
||||||
|
taprootKey1, err := tapscript1.TaprootKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(
|
||||||
|
t, testTaprootKey.SerializeCompressed(),
|
||||||
|
taprootKey1.SerializeCompressed(),
|
||||||
|
)
|
||||||
|
|
||||||
|
addr1, err := w.ImportTaprootScript(scope, tapscript1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, testTapscriptAddr, addr1.Address().String())
|
||||||
|
pkScript, err := txscript.PayToAddrScript(addr1.Address())
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, testTapscriptPkScript, pkScript)
|
||||||
|
|
||||||
|
// Send some coins to the taproot address now and wait until they are
|
||||||
|
// seen as unconfirmed.
|
||||||
|
_, err = miner.SendOutputs([]*wire.TxOut{{
|
||||||
|
Value: btcutil.SatoshiPerBitcoin,
|
||||||
|
PkScript: pkScript,
|
||||||
|
}}, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var utxos []*lnwallet.Utxo
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
utxos, err = w.ListUnspentWitness(0, math.MaxInt32, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return len(utxos) == 1
|
||||||
|
}, time.Minute, 50*time.Millisecond)
|
||||||
|
require.Equal(t, testTapscriptPkScript, utxos[0].PkScript)
|
||||||
|
|
||||||
|
// Now, as a last test, make sure that when we try adding an address
|
||||||
|
// with partial script reveal, we get an error that the address already
|
||||||
|
// exists.
|
||||||
|
tapscript2 := input.TapscriptPartialReveal(
|
||||||
|
testPubKey, leaf1, leaf2.TapHash(),
|
||||||
|
)
|
||||||
|
_, err = w.ImportTaprootScript(scope, tapscript2)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), fmt.Sprintf(
|
||||||
|
"address for script hash/key %x already exists",
|
||||||
|
schnorr.SerializePubKey(testTaprootKey),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
func newTestWallet(t *testing.T, netParams *chaincfg.Params,
|
func newTestWallet(t *testing.T, netParams *chaincfg.Params,
|
||||||
seedBytes []byte) (*BtcWallet, func()) {
|
seedBytes []byte) (*BtcWallet, *rpctest.Harness, func()) {
|
||||||
|
|
||||||
tempDir, err := ioutil.TempDir("", "lnwallet")
|
tempDir, err := ioutil.TempDir("", "lnwallet")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -176,7 +293,7 @@ func newTestWallet(t *testing.T, netParams *chaincfg.Params,
|
||||||
t.Fatalf("creating temp dir failed: %v", err)
|
t.Fatalf("creating temp dir failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chainBackend, backendCleanup := getChainBackend(t, netParams)
|
chainBackend, miner, backendCleanup := getChainBackend(t, netParams)
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
_ = os.RemoveAll(tempDir)
|
_ = os.RemoveAll(tempDir)
|
||||||
backendCleanup()
|
backendCleanup()
|
||||||
|
@ -206,12 +323,12 @@ func newTestWallet(t *testing.T, netParams *chaincfg.Params,
|
||||||
t.Fatalf("starting wallet failed: %v", err)
|
t.Fatalf("starting wallet failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return w, cleanup
|
return w, miner, cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
// getChainBackend returns a simple btcd based chain backend to back the wallet.
|
// getChainBackend returns a simple btcd based chain backend to back the wallet.
|
||||||
func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface,
|
func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface,
|
||||||
func()) {
|
*rpctest.Harness, func()) {
|
||||||
|
|
||||||
miningNode, err := rpctest.New(netParams, nil, nil, "")
|
miningNode, err := rpctest.New(netParams, nil, nil, "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -230,7 +347,7 @@ func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface,
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return chainClient, func() {
|
return chainClient, miningNode, func() {
|
||||||
_ = miningNode.TearDown()
|
_ = miningNode.TearDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue