mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 18:10:34 +01:00
414 lines
12 KiB
Go
414 lines
12 KiB
Go
package itest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"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, customizedKeyFamily, 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,
|
|
family, index int32) (*btcec.PublicKey, error) {
|
|
|
|
ctxt, _ := context.WithTimeout(ctx, defaultTimeout)
|
|
req := &signrpc.KeyLocator{
|
|
KeyFamily: family,
|
|
KeyIndex: index,
|
|
}
|
|
resp, err := node.WalletKitClient.DeriveKey(ctxt, req)
|
|
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
|
|
}
|