lnd/lntest/itest/lnd_signer_test.go
Oliver Gugger c45d0f6b07
itest: add integration tests for tapscript import
We add basic integration test coverage for importing Tapscript
addresses. Note that FundPsbt is not supported with those types of
imported keys, so we need to manually fill in all information in the
PSBT to be able to sweep the funds again.
2022-08-25 09:21:52 +02:00

519 lines
16 KiB
Go

package itest
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/stretchr/testify/require"
)
// testDeriveSharedKey checks the ECDH performed by the endpoint
// DeriveSharedKey. It creates an ephemeral private key, performing an ECDH with
// the node's pubkey and a customized public key to check the validity of the
// result.
func testDeriveSharedKey(net *lntest.NetworkHarness, t *harnessTest) {
runDeriveSharedKey(t, net.Alice)
}
// runDeriveSharedKey checks the ECDH performed by the endpoint
// DeriveSharedKey. It creates an ephemeral private key, performing an ECDH with
// the node's pubkey and a customized public key to check the validity of the
// result.
func runDeriveSharedKey(t *harnessTest, alice *lntest.HarnessNode) {
ctxb := context.Background()
// Create an ephemeral key, extracts its public key, and make a
// PrivKeyECDH using the ephemeral key.
ephemeralPriv, err := btcec.NewPrivateKey()
require.NoError(t.t, err, "failed to create ephemeral key")
ephemeralPubBytes := ephemeralPriv.PubKey().SerializeCompressed()
privKeyECDH := &keychain.PrivKeyECDH{PrivKey: ephemeralPriv}
// assertECDHMatch checks the correctness of the ECDH between the
// ephemeral key and the given public key.
assertECDHMatch := func(pub *btcec.PublicKey,
req *signrpc.SharedKeyRequest) {
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
resp, err := alice.SignerClient.DeriveSharedKey(ctxt, req)
require.NoError(t.t, err, "calling DeriveSharedKey failed")
sharedKey, _ := privKeyECDH.ECDH(pub)
require.Equal(
t.t, sharedKey[:], resp.SharedKey,
"failed to derive the expected key",
)
}
nodePub, err := btcec.ParsePubKey(alice.PubKey[:])
require.NoError(t.t, err, "failed to parse node pubkey")
customizedKeyFamily := int32(keychain.KeyFamilyMultiSig)
customizedIndex := int32(1)
customizedPub, err := deriveCustomizedKey(
ctxb, alice, &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: customizedIndex,
},
)
require.NoError(t.t, err, "failed to create customized pubkey")
// Test DeriveSharedKey with no optional arguments. It will result in
// performing an ECDH between the ephemeral key and the node's pubkey.
req := &signrpc.SharedKeyRequest{EphemeralPubkey: ephemeralPubBytes}
assertECDHMatch(nodePub, req)
// Test DeriveSharedKey with a KeyLoc which points to the node's pubkey.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyLoc: &signrpc.KeyLocator{
KeyFamily: int32(keychain.KeyFamilyNodeKey),
KeyIndex: 0,
},
}
assertECDHMatch(nodePub, req)
// Test DeriveSharedKey with a KeyLoc being set in KeyDesc. The KeyLoc
// points to the node's pubkey.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: int32(keychain.KeyFamilyNodeKey),
KeyIndex: 0,
},
},
}
assertECDHMatch(nodePub, req)
// Test DeriveSharedKey with RawKeyBytes set in KeyDesc. The RawKeyBytes
// is the node's pubkey bytes, and the KeyFamily is KeyFamilyNodeKey.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: alice.PubKey[:],
KeyLoc: &signrpc.KeyLocator{
KeyFamily: int32(keychain.KeyFamilyNodeKey),
},
},
}
assertECDHMatch(nodePub, req)
// Test DeriveSharedKey with a KeyLoc which points to the customized
// public key.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyLoc: &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: customizedIndex,
},
}
assertECDHMatch(customizedPub, req)
// Test DeriveSharedKey with a KeyLoc being set in KeyDesc. The KeyLoc
// points to the customized public key.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: customizedIndex,
},
},
}
assertECDHMatch(customizedPub, req)
// Test DeriveSharedKey with RawKeyBytes set in KeyDesc. The RawKeyBytes
// is the customized public key. The KeyLoc is also set with the family
// being the customizedKeyFamily.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: customizedPub.SerializeCompressed(),
KeyLoc: &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
},
},
}
assertECDHMatch(customizedPub, req)
// assertErrorMatch checks when calling DeriveSharedKey with invalid
// params, the expected error is returned.
assertErrorMatch := func(match string, req *signrpc.SharedKeyRequest) {
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
_, err := alice.SignerClient.DeriveSharedKey(ctxt, req)
require.Error(t.t, err, "expected to have an error")
require.Contains(
t.t, err.Error(), match, "error failed to match",
)
}
// Test that EphemeralPubkey must be supplied.
req = &signrpc.SharedKeyRequest{}
assertErrorMatch("must provide ephemeral pubkey", req)
// Test that cannot use both KeyDesc and KeyLoc.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: customizedPub.SerializeCompressed(),
},
KeyLoc: &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: 0,
},
}
assertErrorMatch("use either key_desc or key_loc", req)
// Test when KeyDesc is used, KeyLoc must be set.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: alice.PubKey[:],
},
}
assertErrorMatch("key_desc.key_loc must also be set", req)
// Test that cannot use both RawKeyBytes and KeyIndex.
req = &signrpc.SharedKeyRequest{
EphemeralPubkey: ephemeralPubBytes,
KeyDesc: &signrpc.KeyDescriptor{
RawKeyBytes: customizedPub.SerializeCompressed(),
KeyLoc: &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: 1,
},
},
}
assertErrorMatch("use either raw_key_bytes or key_index", req)
}
// testSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all
// custom ways of specifying the signing key in the key descriptor/locator.
func testSignOutputRaw(net *lntest.NetworkHarness, t *harnessTest) {
runSignOutputRaw(t, net, net.Alice)
}
// runSignOutputRaw makes sure that the SignOutputRaw RPC can be used with all
// custom ways of specifying the signing key in the key descriptor/locator.
func runSignOutputRaw(t *harnessTest, net *lntest.NetworkHarness,
alice *lntest.HarnessNode) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
// For the next step, we need a public key. Let's use a special family
// for this. We want this to be an index of zero.
const testCustomKeyFamily = 44
keyDesc, err := alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{
KeyFamily: testCustomKeyFamily,
},
)
require.NoError(t.t, err)
require.Equal(t.t, int32(0), keyDesc.KeyLoc.KeyIndex)
targetPubKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(t.t, err)
// First, try with a key descriptor that only sets the public key.
assertSignOutputRaw(
t, net, alice, targetPubKey, &signrpc.KeyDescriptor{
RawKeyBytes: keyDesc.RawKeyBytes,
},
)
// Now try again, this time only with the (0 index!) key locator.
assertSignOutputRaw(
t, net, alice, targetPubKey, &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: keyDesc.KeyLoc.KeyFamily,
KeyIndex: keyDesc.KeyLoc.KeyIndex,
},
},
)
// And now test everything again with a new key where we know the index
// is not 0.
ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
keyDesc, err = alice.WalletKitClient.DeriveNextKey(
ctxt, &walletrpc.KeyReq{
KeyFamily: testCustomKeyFamily,
},
)
require.NoError(t.t, err)
require.Equal(t.t, int32(1), keyDesc.KeyLoc.KeyIndex)
targetPubKey, err = btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(t.t, err)
// First, try with a key descriptor that only sets the public key.
assertSignOutputRaw(
t, net, alice, targetPubKey, &signrpc.KeyDescriptor{
RawKeyBytes: keyDesc.RawKeyBytes,
},
)
// Now try again, this time only with the key locator.
assertSignOutputRaw(
t, net, alice, targetPubKey, &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: keyDesc.KeyLoc.KeyFamily,
KeyIndex: keyDesc.KeyLoc.KeyIndex,
},
},
)
}
// assertSignOutputRaw sends coins to a p2wkh address derived from the given
// target public key and then tries to spend that output again by invoking the
// SignOutputRaw RPC with the key descriptor provided.
func assertSignOutputRaw(t *harnessTest, net *lntest.NetworkHarness,
alice *lntest.HarnessNode, targetPubKey *btcec.PublicKey,
keyDesc *signrpc.KeyDescriptor) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
pubKeyHash := btcutil.Hash160(targetPubKey.SerializeCompressed())
targetAddr, err := btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, harnessNetParams,
)
require.NoError(t.t, err)
targetScript, err := txscript.PayToAddrScript(targetAddr)
require.NoError(t.t, err)
// Send some coins to the generated p2wpkh address.
_, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
Addr: targetAddr.String(),
Amount: 800_000,
})
require.NoError(t.t, err)
// Wait until the TX is found in the mempool.
txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
targetOutputIndex := getOutputIndex(
t, net.Miner, txid, targetAddr.String(),
)
// Clear the mempool.
mineBlocks(t, net, 1, 1)
// Try to spend the output now to a new p2wkh address.
p2wkhResp, err := alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
})
require.NoError(t.t, err)
p2wkhAdrr, err := btcutil.DecodeAddress(
p2wkhResp.Address, harnessNetParams,
)
require.NoError(t.t, err)
p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAdrr)
require.NoError(t.t, err)
tx := wire.NewMsgTx(2)
tx.TxIn = []*wire.TxIn{{
PreviousOutPoint: wire.OutPoint{
Hash: *txid,
Index: uint32(targetOutputIndex),
},
}}
value := int64(800_000 - 200)
tx.TxOut = []*wire.TxOut{{
PkScript: p2wkhPkScript,
Value: value,
}}
var buf bytes.Buffer
require.NoError(t.t, tx.Serialize(&buf))
signResp, err := alice.SignerClient.SignOutputRaw(
ctxt, &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: &signrpc.TxOut{
PkScript: targetScript,
Value: 800_000,
},
InputIndex: 0,
KeyDesc: keyDesc,
Sighash: uint32(txscript.SigHashAll),
WitnessScript: targetScript,
}},
},
)
require.NoError(t.t, err)
tx.TxIn[0].Witness = wire.TxWitness{
append(signResp.RawSigs[0], byte(txscript.SigHashAll)),
targetPubKey.SerializeCompressed(),
}
buf.Reset()
require.NoError(t.t, tx.Serialize(&buf))
_, err = alice.WalletKitClient.PublishTransaction(
ctxt, &walletrpc.Transaction{
TxHex: buf.Bytes(),
},
)
require.NoError(t.t, err)
// Wait until the spending tx is found.
txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
require.NoError(t.t, err)
p2wkhOutputIndex := getOutputIndex(
t, net.Miner, txid, p2wkhAdrr.String(),
)
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(p2wkhOutputIndex),
}
assertWalletUnspent(t, alice, op, "")
// Mine another block to clean up the mempool and to make sure the spend
// tx is actually included in a block.
mineBlocks(t, net, 1, 1)
}
// deriveCustomizedKey uses the family and index to derive a public key from
// the node's walletkit client.
func deriveCustomizedKey(ctx context.Context, node *lntest.HarnessNode,
keyLoc *signrpc.KeyLocator) (*btcec.PublicKey, error) {
ctxt, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
resp, err := node.WalletKitClient.DeriveKey(ctxt, keyLoc)
if err != nil {
return nil, fmt.Errorf("failed to derive key: %v", err)
}
pub, err := btcec.ParsePubKey(resp.RawKeyBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse node pubkey: %v", err)
}
return pub, nil
}
// testSignVerifyMessage makes sure that the SignMessage RPC can be used with
// all custom flags by verifying with VerifyMessage. Tests both ECDSA and
// Schnorr signatures.
func testSignVerifyMessage(net *lntest.NetworkHarness, t *harnessTest) {
runSignVerifyMessage(t, net, net.Alice)
}
// runSignVerifyMessage makes sure that the SignMessage RPC can be used with all
// custom flags by verifying with VerifyMessage. Tests both ECDSA and Schnorr
// signatures.
func runSignVerifyMessage(t *harnessTest, net *lntest.NetworkHarness,
alice *lntest.HarnessNode) {
ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
defer cancel()
aliceMsg := []byte("alice msg")
keyLoc := &signrpc.KeyLocator{
KeyFamily: int32(keychain.KeyFamilyNodeKey),
KeyIndex: 1,
}
// Sign a message with the default ECDSA.
signMsgReq := &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
SchnorrSig: false,
}
signMsgResp, err := alice.SignerClient.SignMessage(ctxt, signMsgReq)
require.NoError(t.t, err, "failed to sign message")
customPubKey, err := deriveCustomizedKey(ctxt, alice, keyLoc)
require.NoError(t.t, err, "failed to create customized pubkey")
verifyReq := &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: customPubKey.SerializeCompressed(),
IsSchnorrSig: false,
}
verifyResp, err := alice.SignerClient.VerifyMessage(ctxt, verifyReq)
require.NoError(t.t, err)
require.True(t.t, verifyResp.Valid, "failed to verify message")
// Use a different key locator.
keyLoc = &signrpc.KeyLocator{
KeyFamily: int32(keychain.KeyFamilyNodeKey),
KeyIndex: 2,
}
// Sign a message with Schnorr signature.
signMsgReq = &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
SchnorrSig: true,
}
signMsgResp, err = alice.SignerClient.SignMessage(ctxt, signMsgReq)
require.NoError(t.t, err)
customPubKey, err = deriveCustomizedKey(ctxt, alice, keyLoc)
require.NoError(t.t, err, "failed to create customized pubkey")
// Verify the Schnorr signature.
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(customPubKey),
IsSchnorrSig: true,
}
verifyResp, err = alice.SignerClient.VerifyMessage(ctxt, verifyReq)
require.NoError(t.t, err)
require.True(t.t, verifyResp.Valid, "failed to verify message")
// Also test that we can tweak a private key and verify the message
// against the tweaked public key.
tweakBytes := sha256.Sum256([]byte("some text"))
tweakedPubKey := txscript.ComputeTaprootOutputKey(
customPubKey, tweakBytes[:],
)
signMsgReq.SchnorrSigTapTweak = tweakBytes[:]
signMsgResp, err = alice.SignerClient.SignMessage(ctxt, signMsgReq)
require.NoError(t.t, err)
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(tweakedPubKey),
IsSchnorrSig: true,
}
verifyResp, err = alice.SignerClient.VerifyMessage(ctxt, verifyReq)
require.NoError(t.t, err)
require.True(t.t, verifyResp.Valid, "failed to verify message")
}