diff --git a/lntemp/harness.go b/lntemp/harness.go index 35f3f2d04..35699a6f3 100644 --- a/lntemp/harness.go +++ b/lntemp/harness.go @@ -2,6 +2,7 @@ package lntemp import ( "context" + "encoding/hex" "fmt" "testing" @@ -20,6 +21,12 @@ import ( "github.com/stretchr/testify/require" ) +const ( + // defaultMinerFeeRate specifies the fee rate in sats when sending + // outputs from the miner. + defaultMinerFeeRate = 7500 +) + // TestCase defines a test case that's been used in the integration test. type TestCase struct { // Name specifies the test name. @@ -179,10 +186,7 @@ func (h *HarnessTest) SetupStandbyNodes() { PkScript: addrScript, Value: 10 * btcutil.SatoshiPerBitcoin, } - _, err = h.Miner.SendOutputs( - []*wire.TxOut{output}, 7500, - ) - require.NoError(h, err, "send output failed") + h.Miner.SendOutput(output, 7500) } } @@ -676,3 +680,81 @@ func (h *HarnessTest) CloseChannel(hn *node.HarnessNode, return h.assertChannelClosed(hn, cp, false, stream) } + +// IsNeutrinoBackend returns a bool indicating whether the node is using a +// neutrino as its backend. This is useful when we want to skip certain tests +// which cannot be done with a neutrino backend. +func (h *HarnessTest) IsNeutrinoBackend() bool { + return h.manager.chainBackend.Name() == NeutrinoBackendName +} + +// fundCoins attempts to send amt satoshis from the internal mining node to the +// targeted lightning node. The confirmed boolean indicates whether the +// transaction that pays to the target should confirm. For neutrino backend, +// the `confirmed` param is ignored. +func (h *HarnessTest) fundCoins(amt btcutil.Amount, target *node.HarnessNode, + addrType lnrpc.AddressType, confirmed bool) { + + initialBalance := target.RPC.WalletBalance() + + // First, obtain an address from the target lightning node, preferring + // to receive a p2wkh address s.t the output can immediately be used as + // an input to a funding transaction. + req := &lnrpc.NewAddressRequest{Type: addrType} + resp := target.RPC.NewAddress(req) + addr := h.DecodeAddress(resp.Address) + addrScript := h.PayToAddrScript(addr) + + // Generate a transaction which creates an output to the target + // pkScript of the desired amount. + output := &wire.TxOut{ + PkScript: addrScript, + Value: int64(amt), + } + h.Miner.SendOutput(output, defaultMinerFeeRate) + + // Encode the pkScript in hex as this the format that it will be + // returned via rpc. + expPkScriptStr := hex.EncodeToString(addrScript) + + // Now, wait for ListUnspent to show the unconfirmed transaction + // containing the correct pkscript. + // + // Since neutrino doesn't support unconfirmed outputs, skip this check. + if !h.IsNeutrinoBackend() { + utxos := h.AssertNumUTXOsUnconfirmed(target, 1) + + // Assert that the lone unconfirmed utxo contains the same + // pkscript as the output generated above. + pkScriptStr := utxos[0].PkScript + require.Equal(h, pkScriptStr, expPkScriptStr, + "pkscript mismatch") + } + + // If the transaction should remain unconfirmed, then we'll wait until + // the target node's unconfirmed balance reflects the expected balance + // and exit. + if !confirmed && !h.IsNeutrinoBackend() { + expectedBalance := btcutil.Amount( + initialBalance.UnconfirmedBalance, + ) + amt + h.WaitForBalanceUnconfirmed(target, expectedBalance) + + return + } + + // Otherwise, we'll generate 2 new blocks to ensure the output gains a + // sufficient number of confirmations and wait for the balance to + // reflect what's expected. + h.Miner.MineBlocks(2) + + expectedBalance := btcutil.Amount(initialBalance.ConfirmedBalance) + amt + h.WaitForBalanceConfirmed(target, expectedBalance) +} + +// FundCoins attempts to send amt satoshis from the internal mining node to the +// targeted lightning node using a P2WKH address. 2 blocks are mined after in +// order to confirm the transaction. +func (h *HarnessTest) FundCoins(amt btcutil.Amount, hn *node.HarnessNode) { + h.fundCoins(amt, hn, lnrpc.AddressType_WITNESS_PUBKEY_HASH, true) +} diff --git a/lntemp/harness_assertion.go b/lntemp/harness_assertion.go index 546020f75..e714b22ce 100644 --- a/lntemp/harness_assertion.go +++ b/lntemp/harness_assertion.go @@ -2,17 +2,24 @@ package lntemp import ( "context" + "crypto/rand" "fmt" + "math" "strings" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntemp/node" "github.com/lightningnetwork/lnd/lntemp/rpc" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntypes" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) // WaitForBlockchainSync waits until the node is synced to chain. @@ -498,3 +505,185 @@ func (h *HarnessTest) WaitForGraphSync(hn *node.HarnessNode) { }, DefaultTimeout) require.NoError(h, err, "%s: timeout while sync to graph", hn.Name()) } + +// AssertNumUTXOsWithConf waits for the given number of UTXOs with the +// specified confirmations range to be available or fails if that isn't the +// case before the default timeout. +// +// NOTE: for standby nodes(Alice and Bob), this method takes account of the +// previous state of the node's UTXOs. The previous state is snapshotted when +// finishing a previous test case via the cleanup function in `Subtest`. In +// other words, this assertion only checks the new changes made in the current +// test. +func (h *HarnessTest) AssertNumUTXOsWithConf(hn *node.HarnessNode, + expectedUtxos int, max, min int32) []*lnrpc.Utxo { + + var unconfirmed bool + + old := hn.State.UTXO.Confirmed + if max == 0 { + old = hn.State.UTXO.Unconfirmed + unconfirmed = true + } + + var utxos []*lnrpc.Utxo + err := wait.NoError(func() error { + req := &walletrpc.ListUnspentRequest{ + Account: "", + MaxConfs: max, + MinConfs: min, + UnconfirmedOnly: unconfirmed, + } + resp := hn.RPC.ListUnspent(req) + total := len(resp.Utxos) + + if total-old == expectedUtxos { + utxos = resp.Utxos[old:] + + return nil + } + + return errNumNotMatched(hn.Name(), "num of UTXOs", + expectedUtxos, total-old, total, old) + }, DefaultTimeout) + require.NoError(h, err, "timeout waiting for UTXOs") + + return utxos +} + +// AssertNumUTXOsUnconfirmed asserts the expected num of unconfirmed utxos are +// seen. +// +// NOTE: for standby nodes(Alice and Bob), this method takes account of the +// previous state of the node's UTXOs. Check `AssertNumUTXOsWithConf` for +// details. +func (h *HarnessTest) AssertNumUTXOsUnconfirmed(hn *node.HarnessNode, + num int) []*lnrpc.Utxo { + + return h.AssertNumUTXOsWithConf(hn, num, 0, 0) +} + +// AssertNumUTXOsConfirmed asserts the expected num of confirmed utxos are +// seen, which means the returned utxos have at least one confirmation. +// +// NOTE: for standby nodes(Alice and Bob), this method takes account of the +// previous state of the node's UTXOs. Check `AssertNumUTXOsWithConf` for +// details. +func (h *HarnessTest) AssertNumUTXOsConfirmed(hn *node.HarnessNode, + num int) []*lnrpc.Utxo { + + return h.AssertNumUTXOsWithConf(hn, num, math.MaxInt32, 1) +} + +// AssertNumUTXOs asserts the expected num of utxos are seen, including +// confirmed and unconfirmed outputs. +// +// NOTE: for standby nodes(Alice and Bob), this method takes account of the +// previous state of the node's UTXOs. Check `AssertNumUTXOsWithConf` for +// details. +func (h *HarnessTest) AssertNumUTXOs(hn *node.HarnessNode, + num int) []*lnrpc.Utxo { + + return h.AssertNumUTXOsWithConf(hn, num, math.MaxInt32, 0) +} + +// WaitForBalanceConfirmed waits until the node sees the expected confirmed +// balance in its wallet. +func (h *HarnessTest) WaitForBalanceConfirmed(hn *node.HarnessNode, + expected btcutil.Amount) { + + var lastBalance btcutil.Amount + err := wait.NoError(func() error { + resp := hn.RPC.WalletBalance() + + lastBalance = btcutil.Amount(resp.ConfirmedBalance) + if lastBalance == expected { + return nil + } + + return fmt.Errorf("expected %v, only have %v", expected, + lastBalance) + }, DefaultTimeout) + + require.NoError(h, err, "timeout waiting for confirmed balances") +} + +// WaitForBalanceUnconfirmed waits until the node sees the expected unconfirmed +// balance in its wallet. +func (h *HarnessTest) WaitForBalanceUnconfirmed(hn *node.HarnessNode, + expected btcutil.Amount) { + + var lastBalance btcutil.Amount + err := wait.NoError(func() error { + resp := hn.RPC.WalletBalance() + + lastBalance = btcutil.Amount(resp.UnconfirmedBalance) + if lastBalance == expected { + return nil + } + + return fmt.Errorf("expected %v, only have %v", expected, + lastBalance) + }, DefaultTimeout) + + require.NoError(h, err, "timeout waiting for unconfirmed balances") +} + +// Random32Bytes generates a random 32 bytes which can be used as a pay hash, +// preimage, etc. +func (h *HarnessTest) Random32Bytes() []byte { + randBuf := make([]byte, lntypes.HashSize) + + _, err := rand.Read(randBuf) + require.NoErrorf(h, err, "internal error, cannot generate random bytes") + + return randBuf +} + +// DecodeAddress decodes a given address and asserts there's no error. +func (h *HarnessTest) DecodeAddress(addr string) btcutil.Address { + resp, err := btcutil.DecodeAddress(addr, harnessNetParams) + require.NoError(h, err, "DecodeAddress failed") + + return resp +} + +// PayToAddrScript creates a new script from the given address and asserts +// there's no error. +func (h *HarnessTest) PayToAddrScript(addr btcutil.Address) []byte { + addrScript, err := txscript.PayToAddrScript(addr) + require.NoError(h, err, "PayToAddrScript failed") + + return addrScript +} + +// AssertChannelBalanceResp makes a ChannelBalance request and checks the +// returned response matches the expected. +func (h *HarnessTest) AssertChannelBalanceResp(hn *node.HarnessNode, + expected *lnrpc.ChannelBalanceResponse) { + + resp := hn.RPC.ChannelBalance() + require.True(h, proto.Equal(expected, resp), "balance is incorrect "+ + "got: %v, want: %v", resp, expected) +} + +// GetChannelByChanPoint tries to find a channel matching the channel point and +// asserts. It returns the channel found. +func (h *HarnessTest) GetChannelByChanPoint(hn *node.HarnessNode, + chanPoint *lnrpc.ChannelPoint) *lnrpc.Channel { + + channel, err := h.findChannel(hn, chanPoint) + require.NoErrorf(h, err, "channel not found using %v", chanPoint) + + return channel +} + +// GetChannelCommitType retrieves the active channel commitment type for the +// given chan point. +func (h *HarnessTest) GetChannelCommitType(hn *node.HarnessNode, + chanPoint *lnrpc.ChannelPoint) lnrpc.CommitmentType { + + c := h.GetChannelByChanPoint(hn, chanPoint) + + return c.CommitmentType +} diff --git a/lntemp/harness_miner.go b/lntemp/harness_miner.go index 7ebf842f1..fc6c8fa3f 100644 --- a/lntemp/harness_miner.go +++ b/lntemp/harness_miner.go @@ -266,3 +266,38 @@ func (h *HarnessMiner) AssertTxInMempool(txid *chainhash.Hash) *wire.MsgTx { require.NoError(h, err, "timeout checking mempool") return msgTx } + +// SendOutputsWithoutChange uses the miner to send the given outputs using the +// specified fee rate and returns the txid. +func (h *HarnessMiner) SendOutputsWithoutChange(outputs []*wire.TxOut, + feeRate btcutil.Amount) *chainhash.Hash { + + txid, err := h.Harness.SendOutputsWithoutChange( + outputs, feeRate, + ) + require.NoErrorf(h, err, "failed to send output") + + return txid +} + +// CreateTransaction uses the miner to create a transaction using the given +// outputs using the specified fee rate and returns the transaction. +func (h *HarnessMiner) CreateTransaction(outputs []*wire.TxOut, + feeRate btcutil.Amount) *wire.MsgTx { + + tx, err := h.Harness.CreateTransaction(outputs, feeRate, false) + require.NoErrorf(h, err, "failed to create transaction") + + return tx +} + +// SendOutput creates, signs, and finally broadcasts a transaction spending +// the harness' available mature coinbase outputs to create the new output. +func (h *HarnessMiner) SendOutput(newOutput *wire.TxOut, + feeRate btcutil.Amount) *chainhash.Hash { + + hash, err := h.Harness.SendOutputs([]*wire.TxOut{newOutput}, feeRate) + require.NoErrorf(h, err, "failed to send outputs") + + return hash +} diff --git a/lntemp/rpc/lnd.go b/lntemp/rpc/lnd.go index 9b82ec03e..3df276d26 100644 --- a/lntemp/rpc/lnd.go +++ b/lntemp/rpc/lnd.go @@ -4,6 +4,7 @@ import ( "context" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" ) // ===================== @@ -214,3 +215,26 @@ func (h *HarnessRPC) CloseChannel( return stream } + +// FundingStateStep makes a RPC call to FundingStateStep and asserts. +func (h *HarnessRPC) FundingStateStep( + msg *lnrpc.FundingTransitionMsg) *lnrpc.FundingStateStepResp { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.LN.FundingStateStep(ctxt, msg) + h.NoError(err, "FundingStateStep") + + return resp +} + +// FundingStateStepAssertErr makes a RPC call to FundingStateStep and asserts +// there's an error. +func (h *HarnessRPC) FundingStateStepAssertErr(m *lnrpc.FundingTransitionMsg) { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + _, err := h.LN.FundingStateStep(ctxt, m) + require.Error(h, err, "expected an error from FundingStateStep") +} diff --git a/lntemp/rpc/wallet_kit.go b/lntemp/rpc/wallet_kit.go index d7b700f0f..c68e38667 100644 --- a/lntemp/rpc/wallet_kit.go +++ b/lntemp/rpc/wallet_kit.go @@ -3,6 +3,7 @@ package rpc import ( "context" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" ) @@ -22,3 +23,27 @@ func (h *HarnessRPC) ListUnspent( return resp } + +// DeriveKey makes a RPC call to the DeriveKey and asserts. +func (h *HarnessRPC) DeriveKey(kl *signrpc.KeyLocator) *signrpc.KeyDescriptor { + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + key, err := h.WalletKit.DeriveKey(ctxt, kl) + h.NoError(err, "DeriveKey") + + return key +} + +// SendOutputs makes a RPC call to the node's WalletKitClient and asserts. +func (h *HarnessRPC) SendOutputs( + req *walletrpc.SendOutputsRequest) *walletrpc.SendOutputsResponse { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + resp, err := h.WalletKit.SendOutputs(ctxt, req) + h.NoError(err, "SendOutputs") + + return resp +}