lnd/itest/lnd_signer_test.go
yyforyongyu 976bb37972
lntest+itest: add method AssertNumTxsInMempool and AssertTxInBlock
in harness

Prepare to make `HarnessTest.Miner` a private instance to sync height.
2024-07-23 21:30:07 +08:00

533 lines
16 KiB
Go

package itest
import (
"bytes"
"context"
"crypto/sha256"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"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/lightningnetwork/lnd/lntest/node"
"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(ht *lntest.HarnessTest) {
runDeriveSharedKey(ht, ht.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(ht *lntest.HarnessTest, alice *node.HarnessNode) {
// Create an ephemeral key, extracts its public key, and make a
// PrivKeyECDH using the ephemeral key.
ephemeralPriv, err := btcec.NewPrivateKey()
require.NoError(ht, 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) {
resp := alice.RPC.DeriveSharedKey(req)
sharedKey, _ := privKeyECDH.ECDH(pub)
require.Equal(ht, sharedKey[:], resp.SharedKey,
"failed to derive the expected key")
}
nodePub, err := btcec.ParsePubKey(alice.PubKey[:])
require.NoError(ht, err, "failed to parse node pubkey")
customizedKeyFamily := int32(keychain.KeyFamilyMultiSig)
customizedIndex := int32(1)
// Derive a customized key.
deriveReq := &signrpc.KeyLocator{
KeyFamily: customizedKeyFamily,
KeyIndex: customizedIndex,
}
resp := alice.RPC.DeriveKey(deriveReq)
customizedPub, err := btcec.ParsePubKey(resp.RawKeyBytes)
require.NoError(ht, err, "failed to parse node 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) {
err := alice.RPC.DeriveSharedKeyErr(req)
require.Contains(ht, err.Error(), match, "error not 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(ht *lntest.HarnessTest) {
runSignOutputRaw(ht, ht.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(ht *lntest.HarnessTest, alice *node.HarnessNode) {
// 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
req := &walletrpc.KeyReq{
KeyFamily: testCustomKeyFamily,
}
keyDesc := alice.RPC.DeriveNextKey(req)
require.Equal(ht, int32(0), keyDesc.KeyLoc.KeyIndex)
targetPubKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(ht, err)
// First, try with a key descriptor that only sets the public key.
assertSignOutputRaw(
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
RawKeyBytes: keyDesc.RawKeyBytes,
}, txscript.SigHashAll,
)
// Now try again, this time only with the (0 index!) key locator.
assertSignOutputRaw(
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: keyDesc.KeyLoc.KeyFamily,
KeyIndex: keyDesc.KeyLoc.KeyIndex,
},
}, txscript.SigHashAll,
)
// And now test everything again with a new key where we know the index
// is not 0.
req = &walletrpc.KeyReq{
KeyFamily: testCustomKeyFamily,
}
keyDesc = alice.RPC.DeriveNextKey(req)
require.Equal(ht, int32(1), keyDesc.KeyLoc.KeyIndex)
targetPubKey, err = btcec.ParsePubKey(keyDesc.RawKeyBytes)
require.NoError(ht, err)
// First, try with a key descriptor that only sets the public key.
assertSignOutputRaw(
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
RawKeyBytes: keyDesc.RawKeyBytes,
}, txscript.SigHashAll,
)
// Now try again, this time only with the key locator.
assertSignOutputRaw(
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: keyDesc.KeyLoc.KeyFamily,
KeyIndex: keyDesc.KeyLoc.KeyIndex,
},
}, txscript.SigHashAll,
)
// Finally, we'll try again, but this time with a non-default sighash.
assertSignOutputRaw(
ht, alice, targetPubKey, &signrpc.KeyDescriptor{
KeyLoc: &signrpc.KeyLocator{
KeyFamily: keyDesc.KeyLoc.KeyFamily,
KeyIndex: keyDesc.KeyLoc.KeyIndex,
},
}, txscript.SigHashSingle,
)
}
// 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(ht *lntest.HarnessTest,
alice *node.HarnessNode, targetPubKey *btcec.PublicKey,
keyDesc *signrpc.KeyDescriptor,
sigHash txscript.SigHashType) {
pubKeyHash := btcutil.Hash160(targetPubKey.SerializeCompressed())
targetAddr, err := btcutil.NewAddressWitnessPubKeyHash(
pubKeyHash, harnessNetParams,
)
require.NoError(ht, err)
targetScript, err := txscript.PayToAddrScript(targetAddr)
require.NoError(ht, err)
// Send some coins to the generated p2wpkh address.
req := &lnrpc.SendCoinsRequest{
Addr: targetAddr.String(),
Amount: 800_000,
TargetConf: 6,
}
alice.RPC.SendCoins(req)
// Wait until the TX is found in the mempool.
txid := ht.AssertNumTxsInMempool(1)[0]
targetOutputIndex := ht.GetOutputIndex(txid, targetAddr.String())
// Clear the mempool.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Try to spend the output now to a new p2wkh address.
addrReq := &lnrpc.NewAddressRequest{Type: AddrTypeWitnessPubkeyHash}
p2wkhResp := alice.RPC.NewAddress(addrReq)
p2wkhAdrr, err := btcutil.DecodeAddress(
p2wkhResp.Address, harnessNetParams,
)
require.NoError(ht, err)
p2wkhPkScript, err := txscript.PayToAddrScript(p2wkhAdrr)
require.NoError(ht, 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(ht, tx.Serialize(&buf))
signReq := &signrpc.SignReq{
RawTxBytes: buf.Bytes(),
SignDescs: []*signrpc.SignDescriptor{{
Output: &signrpc.TxOut{
PkScript: targetScript,
Value: 800_000,
},
InputIndex: 0,
KeyDesc: keyDesc,
Sighash: uint32(sigHash),
WitnessScript: targetScript,
}},
}
signResp := alice.RPC.SignOutputRaw(signReq)
tx.TxIn[0].Witness = wire.TxWitness{
append(signResp.RawSigs[0], byte(sigHash)),
targetPubKey.SerializeCompressed(),
}
buf.Reset()
require.NoError(ht, tx.Serialize(&buf))
alice.RPC.PublishTransaction(&walletrpc.Transaction{
TxHex: buf.Bytes(),
})
// Wait until the spending tx is found.
txid = ht.AssertNumTxsInMempool(1)[0]
p2wkhOutputIndex := ht.GetOutputIndex(txid, p2wkhAdrr.String())
op := &lnrpc.OutPoint{
TxidBytes: txid[:],
OutputIndex: uint32(p2wkhOutputIndex),
}
ht.AssertUTXOInWallet(alice, op, "")
// Mine another block to clean up the mempool and to make sure the
// spend tx is actually included in a block.
ht.MineBlocksAndAssertNumTxes(1, 1)
}
// 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(ht *lntest.HarnessTest) {
runSignVerifyMessage(ht, ht.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(ht *lntest.HarnessTest, alice *node.HarnessNode) {
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 := alice.RPC.SignMessageSigner(signMsgReq)
deriveCustomizedKey := func() *btcec.PublicKey {
resp := alice.RPC.DeriveKey(keyLoc)
pub, err := btcec.ParsePubKey(resp.RawKeyBytes)
require.NoError(ht, err, "failed to parse node pubkey")
return pub
}
customPubKey := deriveCustomizedKey()
verifyReq := &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: customPubKey.SerializeCompressed(),
IsSchnorrSig: false,
}
verifyResp := alice.RPC.VerifyMessageSigner(verifyReq)
require.True(ht, 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 = alice.RPC.SignMessageSigner(signMsgReq)
customPubKey = deriveCustomizedKey()
// Verify the Schnorr signature.
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(customPubKey),
IsSchnorrSig: true,
}
verifyResp = alice.RPC.VerifyMessageSigner(verifyReq)
require.True(ht, 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 = alice.RPC.SignMessageSigner(signMsgReq)
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(tweakedPubKey),
IsSchnorrSig: true,
}
verifyResp = alice.RPC.VerifyMessageSigner(verifyReq)
require.True(ht, verifyResp.Valid, "failed to verify message")
// Now let's try signing and verifying a tagged hash.
tag := []byte("lightninginvoice_requestsignature")
signMsgReq = &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
SchnorrSig: true,
Tag: tag,
}
signMsgResp = alice.RPC.SignMessageSigner(signMsgReq)
customPubKey = deriveCustomizedKey()
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(customPubKey),
IsSchnorrSig: true,
Tag: tag,
}
verifyResp = alice.RPC.VerifyMessageSigner(verifyReq)
require.True(ht, verifyResp.Valid, "failed to verify message")
// Verify that both SignMessage and VerifyMessage error if a tag is
// provided but the Schnorr option is not set.
signMsgReq = &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
Tag: tag,
}
expectedErr := "tag can only be used when the Schnorr signature " +
"option is set"
ctxt := context.Background()
_, err := alice.RPC.Signer.SignMessage(ctxt, signMsgReq)
require.ErrorContains(ht, err, expectedErr)
verifyReq = &signrpc.VerifyMessageReq{
Msg: aliceMsg,
Signature: signMsgResp.Signature,
Pubkey: schnorr.SerializePubKey(customPubKey),
Tag: tag,
}
_, err = alice.RPC.Signer.VerifyMessage(ctxt, verifyReq)
require.ErrorContains(ht, err, expectedErr)
// Make sure that SignMessage throws an error if a BIP0340 or
// TapSighash tag is provided.
signMsgReq = &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
SchnorrSig: true,
Tag: []byte("BIP0340/challenge"),
}
_, err = alice.RPC.Signer.SignMessage(ctxt, signMsgReq)
require.ErrorContains(ht, err, "tag cannot have BIP0340 prefix")
signMsgReq = &signrpc.SignMessageReq{
Msg: aliceMsg,
KeyLoc: keyLoc,
SchnorrSig: true,
Tag: chainhash.TagTapSighash,
}
_, err = alice.RPC.Signer.SignMessage(ctxt, signMsgReq)
require.ErrorContains(ht, err, "tag cannot be TapSighash")
}