lnd version, "hacked" to enable seedless restore from xprv + scb
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

6545 lines
201 KiB

package htlcswitch
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"fmt"
"io"
"net"
"reflect"
"runtime"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/go-errors/errors"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/htlcswitch/hodl"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/ticker"
"github.com/stretchr/testify/require"
)
const (
testStartingHeight = 100
testDefaultDelta = 6
)
// concurrentTester is a thread-safe wrapper around the Fatalf method of a
// *testing.T instance. With this wrapper multiple goroutines can safely
// attempt to fail a test concurrently.
type concurrentTester struct {
mtx sync.Mutex
*testing.T
}
func newConcurrentTester(t *testing.T) *concurrentTester {
return &concurrentTester{
T: t,
}
}
func (c *concurrentTester) Fatalf(format string, args ...interface{}) {
c.T.Helper()
c.mtx.Lock()
defer c.mtx.Unlock()
c.T.Fatalf(format, args...)
}
// messageToString is used to produce less spammy log messages in trace mode by
// setting the 'Curve" parameter to nil. Doing this avoids printing out each of
// the field elements in the curve parameters for secp256k1.
func messageToString(msg lnwire.Message) string {
switch m := msg.(type) {
case *lnwire.RevokeAndAck:
m.NextRevocationKey.Curve = nil
case *lnwire.AcceptChannel:
m.FundingKey.Curve = nil
m.RevocationPoint.Curve = nil
m.PaymentPoint.Curve = nil
m.DelayedPaymentPoint.Curve = nil
m.FirstCommitmentPoint.Curve = nil
case *lnwire.OpenChannel:
m.FundingKey.Curve = nil
m.RevocationPoint.Curve = nil
m.PaymentPoint.Curve = nil
m.DelayedPaymentPoint.Curve = nil
m.FirstCommitmentPoint.Curve = nil
case *lnwire.FundingLocked:
m.NextPerCommitmentPoint.Curve = nil
}
return spew.Sdump(msg)
}
// expectedMessage struct holds the message which travels from one peer to
// another, and additional information like, should this message we skipped for
// handling.
type expectedMessage struct {
from string
to string
message lnwire.Message
skip bool
}
// createLogFunc is a helper function which returns the function which will be
// used for logging message are received from another peer.
func createLogFunc(name string, channelID lnwire.ChannelID) messageInterceptor {
return func(m lnwire.Message) (bool, error) {
chanID, err := getChanID(m)
if err != nil {
return false, err
}
if chanID == channelID {
fmt.Printf("---------------------- \n %v received: "+
"%v", name, messageToString(m))
}
return false, nil
}
}
// createInterceptorFunc creates the function by the given set of messages
// which, checks the order of the messages and skip the ones which were
// indicated to be intercepted.
func createInterceptorFunc(prefix, receiver string, messages []expectedMessage,
chanID lnwire.ChannelID, debug bool) messageInterceptor {
// Filter message which should be received with given peer name.
var expectToReceive []expectedMessage
for _, message := range messages {
if message.to == receiver {
expectToReceive = append(expectToReceive, message)
}
}
// Return function which checks the message order and skip the
// messages.
return func(m lnwire.Message) (bool, error) {
messageChanID, err := getChanID(m)
if err != nil {
return false, err
}
if messageChanID == chanID {
if len(expectToReceive) == 0 {
return false, errors.Errorf("%v received "+
"unexpected message out of range: %v",
receiver, m.MsgType())
}
expectedMessage := expectToReceive[0]
expectToReceive = expectToReceive[1:]
if expectedMessage.message.MsgType() != m.MsgType() {
return false, errors.Errorf("%v received wrong message: \n"+
"real: %v\nexpected: %v", receiver, m.MsgType(),
expectedMessage.message.MsgType())
}
if debug {
var postfix string
if revocation, ok := m.(*lnwire.RevokeAndAck); ok {
var zeroHash chainhash.Hash
if bytes.Equal(zeroHash[:], revocation.Revocation[:]) {
postfix = "- empty revocation"
}
}
if expectedMessage.skip {
fmt.Printf("skipped: %v: %v %v \n", prefix,
m.MsgType(), postfix)
} else {
fmt.Printf("%v: %v %v \n", prefix, m.MsgType(), postfix)
}
}
return expectedMessage.skip, nil
}
return false, nil
}
}
// TestChannelLinkRevThenSig tests that if a link owes both a revocation and a
// signature to the counterparty (in this order), that they are sent as rev and
// then sig.
//
// Specifically, this tests the following scenario:
//
// A B
// <----add-----
// -----add---->
// <----sig-----
// -----rev----x
// -----sig----x
func TestChannelLinkRevThenSig(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
require.NoError(t, err)
defer cleanUp()
err = start()
require.NoError(t, err)
defer aliceLink.Stop()
alice := newPersistentLinkHarness(
t, aliceLink, batchTicker, restore,
)
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
bobHtlc1 := generateHtlc(t, coreLink, 0)
// <-----add-----
// Send an htlc from Bob to Alice.
ctx.sendHtlcBobToAlice(bobHtlc1)
aliceHtlc1, _ := generateHtlcAndInvoice(t, 0)
// ------add---->
ctx.sendHtlcAliceToBob(0, aliceHtlc1)
ctx.receiveHtlcAliceToBob()
// <-----sig-----
ctx.sendCommitSigBobToAlice(1)
// ------rev----x
var msg lnwire.Message
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
_, ok := msg.(*lnwire.RevokeAndAck)
require.True(t, ok)
// ------sig----x
// Trigger a commitsig from Alice->Bob.
select {
case batchTicker <- time.Now():
case <-time.After(5 * time.Second):
t.Fatalf("could not force commit sig")
}
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
comSig, ok := msg.(*lnwire.CommitSig)
require.True(t, ok)
if len(comSig.HtlcSigs) != 2 {
t.Fatalf("expected 2 htlc sigs, got %d", len(comSig.HtlcSigs))
}
// Restart Alice so she sends and accepts ChannelReestablish.
cleanUp = alice.restart(false, true)
defer cleanUp()
ctx.aliceLink = alice.link
ctx.aliceMsgs = alice.msgs
// Restart Bob as well by calling NewLightningChannel.
bobSigner := bobChannel.Signer
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
bobChannel, err = lnwallet.NewLightningChannel(
bobSigner, bobChannel.State(), bobPool,
)
require.NoError(t, err)
err = bobPool.Start()
require.NoError(t, err)
ctx.bobChannel = bobChannel
// --reestablish->
select {
case msg = <-ctx.aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
_, ok = msg.(*lnwire.ChannelReestablish)
require.True(t, ok)
// <-reestablish--
bobReest, err := bobChannel.State().ChanSyncMsg()
require.NoError(t, err)
ctx.aliceLink.HandleChannelUpdate(bobReest)
// ------rev---->
ctx.receiveRevAndAckAliceToBob()
// ------add---->
ctx.receiveHtlcAliceToBob()
// ------sig---->
ctx.receiveCommitSigAliceToBob(2)
}
// TestChannelLinkSigThenRev tests that if a link owes both a signature and a
// revocation to the counterparty (in this order), that they are sent as sig
// and then rev.
//
// Specifically, this tests the following scenario:
//
// A B
// <----add-----
// -----add---->
// -----sig----x
// <----sig-----
// -----rev----x
func TestChannelLinkSigThenRev(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
require.NoError(t, err)
defer cleanUp()
err = start()
require.NoError(t, err)
defer aliceLink.Stop()
alice := newPersistentLinkHarness(
t, aliceLink, batchTicker, restore,
)
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
bobHtlc1 := generateHtlc(t, coreLink, 0)
// <-----add-----
// Send an htlc from Bob to Alice.
ctx.sendHtlcBobToAlice(bobHtlc1)
aliceHtlc1, _ := generateHtlcAndInvoice(t, 0)
// ------add---->
ctx.sendHtlcAliceToBob(0, aliceHtlc1)
ctx.receiveHtlcAliceToBob()
// ------sig----x
// Trigger a commitsig from Alice->Bob.
select {
case batchTicker <- time.Now():
case <-time.After(5 * time.Second):
t.Fatalf("could not force commit sig")
}
var msg lnwire.Message
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
comSig, ok := msg.(*lnwire.CommitSig)
require.True(t, ok)
if len(comSig.HtlcSigs) != 1 {
t.Fatalf("expected 1 htlc sig, got %d", len(comSig.HtlcSigs))
}
// <-----sig-----
ctx.sendCommitSigBobToAlice(1)
// ------rev----x
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
_, ok = msg.(*lnwire.RevokeAndAck)
require.True(t, ok)
// Restart Alice so she sends and accepts ChannelReestablish.
cleanUp = alice.restart(false, true)
defer cleanUp()
ctx.aliceLink = alice.link
ctx.aliceMsgs = alice.msgs
// Restart Bob as well by calling NewLightningChannel.
bobSigner := bobChannel.Signer
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
bobChannel, err = lnwallet.NewLightningChannel(
bobSigner, bobChannel.State(), bobPool,
)
require.NoError(t, err)
err = bobPool.Start()
require.NoError(t, err)
ctx.bobChannel = bobChannel
// --reestablish->
select {
case msg = <-ctx.aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
_, ok = msg.(*lnwire.ChannelReestablish)
require.True(t, ok)
// <-reestablish--
bobReest, err := bobChannel.State().ChanSyncMsg()
require.NoError(t, err)
ctx.aliceLink.HandleChannelUpdate(bobReest)
// ------add---->
ctx.receiveHtlcAliceToBob()
// ------sig---->
ctx.receiveCommitSigAliceToBob(1)
// ------rev---->
ctx.receiveRevAndAckAliceToBob()
}
// TestChannelLinkSingleHopPayment in this test we checks the interaction
// between Alice and Bob within scope of one channel.
func TestChannelLinkSingleHopPayment(t *testing.T) {
t.Parallel()
// Setup a alice-bob network.
alice, bob, cleanUp, err := createTwoClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newTwoHopNetwork(
t, alice.channel, bob.channel, testStartingHeight,
)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
bobBandwidthBefore := n.bobChannelLink.Bandwidth()
debug := false
if debug {
// Log message that alice receives.
n.aliceServer.intersect(createLogFunc("alice",
n.aliceChannelLink.ChanID()))
// Log message that bob receives.
n.bobServer.intersect(createLogFunc("bob",
n.bobChannelLink.ChanID()))
}
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.bobChannelLink)
// Wait for:
// * HTLC add request to be sent to bob.
// * alice<->bob commitment state to be updated.
// * settle request to be sent back from bob to alice.
// * alice<->bob commitment state to be updated.
// * user notification to be sent.
receiver := n.bobServer
firstHop := n.bobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to make the payment: %v", err)
}
// Wait for Alice to receive the revocation.
//
// TODO(roasbeef); replace with select over returned err chan
time.Sleep(2 * time.Second)
// Check that alice invoice was settled and bandwidth of HTLC
// links was changed.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State != channeldb.ContractSettled {
t.Fatal("alice invoice wasn't settled")
}
if aliceBandwidthBefore-amount != n.aliceChannelLink.Bandwidth() {
t.Fatal("alice bandwidth should have decrease on payment " +
"amount")
}
if bobBandwidthBefore+amount != n.bobChannelLink.Bandwidth() {
t.Fatalf("bob bandwidth isn't match: expected %v, got %v",
bobBandwidthBefore+amount,
n.bobChannelLink.Bandwidth())
}
}
// TestChannelLinkMultiHopPayment checks the ability to send payment over two
// hops. In this test we send the payment from Carol to Alice over Bob peer.
// (Carol -> Bob -> Alice) and checking that HTLC was settled properly and
// balances were changed in two channels.
//
// The test is executed with two different OutgoingCltvRejectDelta values for
// bob. In addition to a normal positive value, we also test the zero case
// because this is currently the configured value in lnd
// (defaultOutgoingCltvRejectDelta).
func TestChannelLinkMultiHopPayment(t *testing.T) {
t.Run(
"bobOutgoingCltvRejectDelta 3",
func(t *testing.T) {
testChannelLinkMultiHopPayment(t, 3)
},
)
t.Run(
"bobOutgoingCltvRejectDelta 0",
func(t *testing.T) {
testChannelLinkMultiHopPayment(t, 0)
},
)
}
func testChannelLinkMultiHopPayment(t *testing.T,
bobOutgoingCltvRejectDelta uint32) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
n.firstBobChannelLink.cfg.OutgoingCltvRejectDelta =
bobOutgoingCltvRejectDelta
n.secondBobChannelLink.cfg.OutgoingCltvRejectDelta =
bobOutgoingCltvRejectDelta
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
debug := false
if debug {
// Log messages that alice receives from bob.
n.aliceServer.intersect(createLogFunc("[alice]<-bob<-carol: ",
n.aliceChannelLink.ChanID()))
// Log messages that bob receives from alice.
n.bobServer.intersect(createLogFunc("alice->[bob]->carol: ",
n.firstBobChannelLink.ChanID()))
// Log messages that bob receives from carol.
n.bobServer.intersect(createLogFunc("alice<-[bob]<-carol: ",
n.secondBobChannelLink.ChanID()))
// Log messages that carol receives from bob.
n.carolServer.intersect(createLogFunc("alice->bob->[carol]",
n.carolChannelLink.ChanID()))
}
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount,
testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// Wait for:
// * HTLC add request to be sent from Alice to Bob.
// * Alice<->Bob commitment states to be updated.
// * HTLC add request to be propagated to Carol.
// * Bob<->Carol commitment state to be updated.
// * settle request to be sent back from Carol to Bob.
// * Alice<->Bob commitment state to be updated.
// * settle request to be sent back from Bob to Alice.
// * Alice<->Bob commitment states to be updated.
// * user notification to be sent.
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// Wait for Alice and Bob's second link to receive the revocation.
time.Sleep(2 * time.Second)
// Check that Carol invoice was settled and bandwidth of HTLC
// links were changed.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State != channeldb.ContractSettled {
t.Fatal("carol invoice haven't been settled")
}
expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
if expectedAliceBandwidth != n.aliceChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth())
}
expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
if expectedBobBandwidth1 != n.firstBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth())
}
expectedBobBandwidth2 := secondBobBandwidthBefore - amount
if expectedBobBandwidth2 != n.secondBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth())
}
expectedCarolBandwidth := carolBandwidthBefore + amount
if expectedCarolBandwidth != n.carolChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth())
}
}
// TestChannelLinkCancelFullCommitment tests the ability for links to cancel
// forwarded HTLCs once all of their commitment slots are full.
func TestChannelLinkCancelFullCommitment(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newTwoHopNetwork(
t, channels.aliceToBob, channels.bobToAlice, testStartingHeight,
)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// Fill up the commitment from Alice's side with 20 sat payments.
count := (input.MaxHTLCNumber / 2)
amt := lnwire.NewMSatFromSatoshis(20000)
htlcAmt, totalTimelock, hopsForwards := generateHops(amt,
testStartingHeight, n.bobChannelLink)
firstHop := n.aliceChannelLink.ShortChanID()
// Create channels to buffer the preimage and error channels used in
// making the preliminary payments.
preimages := make([]lntypes.Preimage, count)
aliceErrChan := make(chan chan error, count)
var wg sync.WaitGroup
for i := 0; i < count; i++ {
// Deterministically generate preimages. Avoid the all-zeroes
// preimage because that will be rejected by the database.
preimages[i] = lntypes.Preimage{byte(i >> 8), byte(i), 1}
wg.Add(1)
go func(i int) {
defer wg.Done()
errChan := n.makeHoldPayment(
n.aliceServer, n.bobServer, firstHop,
hopsForwards, amt, htlcAmt, totalTimelock,
preimages[i],
)
aliceErrChan <- errChan
}(i)
}
// Wait for Alice to finish filling her commitment.
wg.Wait()
close(aliceErrChan)
// Now make an additional payment from Alice to Bob, this should be
// canceled because the commitment in this direction is full.
err = <-makePayment(
n.aliceServer, n.bobServer, firstHop, hopsForwards, amt,
htlcAmt, totalTimelock,
).err
if err == nil {
t.Fatalf("overflow payment should have failed")
}
lerr, ok := err.(*LinkError)
if !ok {
t.Fatalf("expected LinkError, got: %T", err)
}
msg := lerr.WireMessage()
if _, ok := msg.(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected TemporaryChannelFailure, got: %T", msg)
}
// Now, settle all htlcs held by bob and clear the commitment of htlcs.
for _, preimage := range preimages {
preimage := preimage
// It's possible that the HTLCs have not been delivered to the
// invoice registry at this point, so we poll until we are able
// to settle.
err = wait.NoError(func() error {
return n.bobServer.registry.SettleHodlInvoice(preimage)
}, time.Minute)
if err != nil {
t.Fatal(err)
}
}
// Ensure that all of the payments sent by alice eventually succeed.
for errChan := range aliceErrChan {
err := <-errChan
if err != nil {
t.Fatalf("alice payment failed: %v", err)
}
}
}
// TestExitNodeTimelockPayloadMismatch tests that when an exit node receives an
// incoming HTLC, if the time lock encoded in the payload of the forwarded HTLC
// doesn't match the expected payment value, then the HTLC will be rejected
// with the appropriate error.
func TestExitNodeTimelockPayloadMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
const amount = btcutil.SatoshiPerBitcoin
htlcAmt, htlcExpiry, hops := generateHops(amount,
testStartingHeight, n.firstBobChannelLink)
// In order to exercise this case, we'll now _manually_ modify the
// per-hop payload for outgoing time lock to be the incorrect value.
// The proper value of the outgoing CLTV should be the policy set by
// the receiving node, instead we set it to be a random value.
hops[0].FwdInfo.OutgoingCTLV = 500
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailFinalIncorrectCltvExpiry:
default:
t.Fatalf("incorrect error, expected incorrect cltv expiry, "+
"instead have: %v", err)
}
}
// TestExitNodeAmountPayloadMismatch tests that when an exit node receives an
// incoming HTLC, if the amount encoded in the onion payload of the forwarded
// HTLC doesn't match the expected payment value, then the HTLC will be
// rejected.
func TestExitNodeAmountPayloadMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
const amount = btcutil.SatoshiPerBitcoin
htlcAmt, htlcExpiry, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink)
// In order to exercise this case, we'll now _manually_ modify the
// per-hop payload for amount to be the incorrect value. The proper
// value of the amount to forward should be the amount that the
// receiving node expects to receive.
hops[0].FwdInfo.AmountToForward = 1
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
assertFailureCode(t, err, lnwire.CodeFinalIncorrectHtlcAmount)
}
// TestLinkForwardTimelockPolicyMismatch tests that if a node is an
// intermediate node in a multi-hop payment, and receives an HTLC which
// violates its specified multi-hop policy, then the HTLC is rejected.
func TestLinkForwardTimelockPolicyMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// We'll be sending 1 BTC over a 2-hop (3 vertex) route.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
// Generate the route over two hops, ignoring the total time lock that
// we'll need to use for the first HTLC in order to have a sufficient
// time-lock value to account for the decrements over the entire route.
htlcAmt, htlcExpiry, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
htlcExpiry -= 2
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
htlcExpiry,
).Wait(30 * time.Second)
// We should get an error, and that error should indicate that the HTLC
// should be rejected due to a policy violation.
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailIncorrectCltvExpiry:
default:
t.Fatalf("incorrect error, expected incorrect cltv expiry, "+
"instead have: %v", err)
}
}
// TestLinkForwardFeePolicyMismatch tests that if a node is an intermediate
// node in a multi-hop payment and receives an HTLC that violates its current
// fee policy, then the HTLC is rejected with the proper error.
func TestLinkForwardFeePolicyMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// We'll be sending 1 BTC over a 2-hop (3 vertex) route. Given the
// current default fee of 1 SAT, if we just send a single BTC over in
// an HTLC, it should be rejected.
amountNoFee := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
// Generate the route over two hops, ignoring the amount we _should_
// actually send in order to be able to cover fees.
_, htlcExpiry, hops := generateHops(amountNoFee, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amountNoFee,
amountNoFee, htlcExpiry,
).Wait(30 * time.Second)
// We should get an error, and that error should indicate that the HTLC
// should be rejected due to a policy violation.
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailFeeInsufficient:
default:
t.Fatalf("incorrect error, expected fee insufficient, "+
"instead have: %T", err)
}
}
// TestLinkForwardFeePolicyMismatch tests that if a node is an intermediate
// node and receives an HTLC which is _below_ its min HTLC policy, then the
// HTLC will be rejected.
func TestLinkForwardMinHTLCPolicyMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// The current default global min HTLC policy set in the default config
// for the three-hop-network is 5 SAT. So in order to trigger this
// failure mode, we'll create an HTLC with 1 satoshi.
amountNoFee := lnwire.NewMSatFromSatoshis(1)
// With the amount set, we'll generate a route over 2 hops within the
// network that attempts to pay out our specified amount.
htlcAmt, htlcExpiry, hops := generateHops(amountNoFee, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// Next, we'll make the payment which'll send an HTLC with our
// specified parameters to the first hop in the route.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
// We should get an error, and that error should indicate that the HTLC
// should be rejected due to a policy violation (below min HTLC).
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailAmountBelowMinimum:
default:
t.Fatalf("incorrect error, expected amount below minimum, "+
"instead have: %v", err)
}
}
// TestLinkForwardMaxHTLCPolicyMismatch tests that if a node is an intermediate
// node and receives an HTLC which is _above_ its max HTLC policy then the
// HTLC will be rejected.
func TestLinkForwardMaxHTLCPolicyMismatch(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5, btcutil.SatoshiPerBitcoin*5,
)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(
t, channels.aliceToBob, channels.bobToAlice, channels.bobToCarol,
channels.carolToBob, testStartingHeight,
)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// In order to trigger this failure mode, we'll update our policy to have
// a new max HTLC of 10 satoshis.
maxHtlc := lnwire.NewMSatFromSatoshis(10)
// First we'll generate a route over 2 hops within the network that
// attempts to pay out an amount greater than the max HTLC we're about to
// set.
amountNoFee := maxHtlc + 1
htlcAmt, htlcExpiry, hops := generateHops(
amountNoFee, testStartingHeight, n.firstBobChannelLink,
n.carolChannelLink,
)
// We'll now update Bob's policy to set the max HTLC we chose earlier.
n.secondBobChannelLink.cfg.FwrdingPolicy.MaxHTLC = maxHtlc
// Finally, we'll make the payment which'll send an HTLC with our
// specified parameters.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
// We should get an error indicating a temporary channel failure, The
// failure is temporary because this payment would be allowed if Bob
// updated his policy to increase the max HTLC.
if err == nil {
t.Fatalf("payment should have failed but didn't")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailTemporaryChannelFailure:
default:
t.Fatalf("incorrect error, expected temporary channel failure, "+
"instead have: %v", err)
}
}
// TestUpdateForwardingPolicy tests that the forwarding policy for a link is
// able to be updated properly. We'll first create an HTLC that meets the
// specified policy, assert that it succeeds, update the policy (to invalidate
// the prior HTLC), and then ensure that the HTLC is rejected.
func TestUpdateForwardingPolicy(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
amountNoFee := lnwire.NewMSatFromSatoshis(10)
htlcAmt, htlcExpiry, hops := generateHops(amountNoFee,
testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// First, send this 10 mSAT payment over the three hops, the payment
// should succeed, and all balances should be updated accordingly.
firstHop := n.firstBobChannelLink.ShortChanID()
payResp, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// Carol's invoice should now be shown as settled as the payment
// succeeded.
invoice, err := n.carolServer.registry.LookupInvoice(payResp)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State != channeldb.ContractSettled {
t.Fatal("carol invoice haven't been settled")
}
expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
if expectedAliceBandwidth != n.aliceChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth())
}
expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
if expectedBobBandwidth1 != n.firstBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth())
}
expectedBobBandwidth2 := secondBobBandwidthBefore - amountNoFee
if expectedBobBandwidth2 != n.secondBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth())
}
expectedCarolBandwidth := carolBandwidthBefore + amountNoFee
if expectedCarolBandwidth != n.carolChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth())
}
// Now we'll update Bob's policy to jack up his free rate to an extent
// that'll cause him to reject the same HTLC that we just sent.
//
// TODO(roasbeef): should implement grace period within link policy
// update logic
newPolicy := n.globalPolicy
newPolicy.BaseFee = lnwire.NewMSatFromSatoshis(1000)
n.secondBobChannelLink.UpdateForwardingPolicy(newPolicy)
// Next, we'll send the payment again, using the exact same per-hop
// payload for each node. This payment should fail as it won't factor
// in Bob's new fee policy.
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
if err == nil {
t.Fatalf("payment should've been rejected")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got (%T): %v", err, err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailFeeInsufficient:
default:
t.Fatalf("expected FailFeeInsufficient instead got: %v", err)
}
// Reset the policy so we can then test updating the max HTLC policy.
n.secondBobChannelLink.UpdateForwardingPolicy(n.globalPolicy)
// As a sanity check, ensure the original payment now succeeds again.
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// Now we'll update Bob's policy to lower his max HTLC to an extent
// that'll cause him to reject the same HTLC that we just sent.
newPolicy = n.globalPolicy
newPolicy.MaxHTLC = amountNoFee - 1
n.secondBobChannelLink.UpdateForwardingPolicy(newPolicy)
// Next, we'll send the payment again, using the exact same per-hop
// payload for each node. This payment should fail as it won't factor
// in Bob's new max HTLC policy.
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amountNoFee,
htlcAmt, htlcExpiry,
).Wait(30 * time.Second)
if err == nil {
t.Fatalf("payment should've been rejected")
}
rtErr, ok = err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got (%T): %v",
err, err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailTemporaryChannelFailure:
default:
t.Fatalf("expected TemporaryChannelFailure, instead got: %v",
err)
}
}
// TestChannelLinkMultiHopInsufficientPayment checks that we receive error if
// bob<->alice channel has insufficient BTC capacity/bandwidth. In this test we
// send the payment from Carol to Alice over Bob peer. (Carol -> Bob -> Alice)
func TestChannelLinkMultiHopInsufficientPayment(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
// We'll attempt to send 4 BTC although the alice-to-bob channel only
// has 3 BTC total capacity. As a result, this payment should be
// rejected.
amount := lnwire.NewMSatFromSatoshis(4 * btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// Wait for:
// * HTLC add request to be sent to from Alice to Bob.
// * Alice<->Bob commitment states to be updated.
// * Bob trying to add HTLC add request in Bob<->Carol channel.
// * Cancel HTLC request to be sent back from Bob to Alice.
// * user notification to be sent.
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err == nil {
t.Fatal("error haven't been received")
}
assertFailureCode(t, err, lnwire.CodeTemporaryChannelFailure)
// Wait for Alice to receive the revocation.
//
// TODO(roasbeef): add in ntfn hook for state transition completion
time.Sleep(100 * time.Millisecond)
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State == channeldb.ContractSettled {
t.Fatal("carol invoice have been settled")
}
if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore {
t.Fatal("the bandwidth of alice channel link which handles " +
"alice->bob channel should be the same")
}
if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"alice->bob channel should be the same")
}
if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"bob->carol channel should be the same")
}
if n.carolChannelLink.Bandwidth() != carolBandwidthBefore {
t.Fatal("the bandwidth of carol channel link which handles " +
"bob->carol channel should be the same")
}
}
// TestChannelLinkMultiHopUnknownPaymentHash checks that we receive remote error
// from Alice if she received not suitable payment hash for htlc.
func TestChannelLinkMultiHopUnknownPaymentHash(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
blob, err := generateRoute(hops...)
if err != nil {
t.Fatal(err)
}
// Generate payment invoice and htlc, but don't add this invoice to the
// receiver registry. This should trigger an unknown payment hash
// failure.
_, htlc, pid, err := generatePayment(
amount, htlcAmt, totalTimelock, blob,
)
if err != nil {
t.Fatal(err)
}
// Send payment and expose err channel.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
if err != nil {
t.Fatalf("unable to get send payment: %v", err)
}
resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult(
pid, htlc.PaymentHash, newMockDeobfuscator(),
)
if err != nil {
t.Fatalf("unable to get payment result: %v", err)
}
var result *PaymentResult
var ok bool
select {
case result, ok = <-resultChan:
if !ok {
t.Fatalf("unexpected shutdown")
}
case <-time.After(10 * time.Second):
t.Fatalf("no result arrive")
}
assertFailureCode(
t, result.Error, lnwire.CodeIncorrectOrUnknownPaymentDetails,
)
// Wait for Alice to receive the revocation.
require.Eventually(t, func() bool {
if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore {
return false
}
if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore {
return false
}
if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore {
return false
}
if n.carolChannelLink.Bandwidth() != carolBandwidthBefore {
return false
}
return true
}, 10*time.Second, 100*time.Millisecond)
}
// TestChannelLinkMultiHopUnknownNextHop construct the chain of hops
// Carol<->Bob<->Alice and checks that we receive remote error from Bob if he
// has no idea about next hop (hop might goes down and routing info not updated
// yet).
func TestChannelLinkMultiHopUnknownNextHop(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
// Remove bob's outgoing link with Carol. This will cause him to fail
// back the payment to Alice since he is unaware of Carol when the
// payment comes across.
bobChanID := lnwire.NewChanIDFromOutPoint(
&channels.bobToCarol.State().FundingOutpoint,
)
n.bobServer.htlcSwitch.RemoveLink(bobChanID)
firstHop := n.firstBobChannelLink.ShortChanID()
receiver := n.carolServer
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock).Wait(30 * time.Second)
if err == nil {
t.Fatal("error haven't been received")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected ClearTextError")
}
if _, ok = rtErr.WireMessage().(*lnwire.FailUnknownNextPeer); !ok {
t.Fatalf("wrong error has been received: %T",
rtErr.WireMessage())
}
// Wait for Alice to receive the revocation.
//
// TODO(roasbeef): add in ntfn hook for state transition completion
time.Sleep(100 * time.Millisecond)
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State == channeldb.ContractSettled {
t.Fatal("carol invoice have been settled")
}
if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore {
t.Fatal("the bandwidth of alice channel link which handles " +
"alice->bob channel should be the same")
}
if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"alice->bob channel should be the same")
}
if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"bob->carol channel should be the same")
}
if n.carolChannelLink.Bandwidth() != carolBandwidthBefore {
t.Fatal("the bandwidth of carol channel link which handles " +
"bob->carol channel should be the same")
}
// Load the forwarding packages for Bob's incoming link. The payment
// should have been rejected by the switch, and the AddRef in this link
// should be acked by the failed payment.
bobInFwdPkgs, err := channels.bobToAlice.State().LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load bob's fwd pkgs: %v", err)
}
// There should be exactly two forward packages, as a full state
// transition requires two commitment dances.
if len(bobInFwdPkgs) != 2 {
t.Fatalf("bob should have exactly 2 fwdpkgs, has %d",
len(bobInFwdPkgs))
}
// Only one of the forwarding package should have an Add in it, the
// other will be empty. Either way, both AckFilters should be fully
// acked.
for _, fwdPkg := range bobInFwdPkgs {
if !fwdPkg.AckFilter.IsFull() {
t.Fatalf("fwdpkg chanid=%v height=%d AckFilter is not "+
"fully acked", fwdPkg.Source, fwdPkg.Height)
}
}
}
// TestChannelLinkMultiHopDecodeError checks that we send HTLC cancel if
// decoding of onion blob failed.
func TestChannelLinkMultiHopDecodeError(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
// Replace decode function with another which throws an error.
n.carolChannelLink.cfg.ExtractErrorEncrypter = func(
*btcec.PublicKey) (hop.ErrorEncrypter, lnwire.FailCode) {
return nil, lnwire.CodeInvalidOnionVersion
}
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err == nil {
t.Fatal("error haven't been received")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T", err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailInvalidOnionVersion:
default:
t.Fatalf("wrong error have been received: %v", err)
}
// Wait for Bob to receive the revocation.
time.Sleep(100 * time.Millisecond)
// Check that alice invoice wasn't settled and bandwidth of htlc
// links hasn't been changed.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State == channeldb.ContractSettled {
t.Fatal("carol invoice have been settled")
}
if n.aliceChannelLink.Bandwidth() != aliceBandwidthBefore {
t.Fatal("the bandwidth of alice channel link which handles " +
"alice->bob channel should be the same")
}
if n.firstBobChannelLink.Bandwidth() != firstBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"alice->bob channel should be the same")
}
if n.secondBobChannelLink.Bandwidth() != secondBobBandwidthBefore {
t.Fatal("the bandwidth of bob channel link which handles " +
"bob->carol channel should be the same")
}
if n.carolChannelLink.Bandwidth() != carolBandwidthBefore {
t.Fatal("the bandwidth of carol channel link which handles " +
"bob->carol channel should be the same")
}
}
// TestChannelLinkExpiryTooSoonExitNode tests that if we send an HTLC to a node
// with an expiry that is already expired, or too close to the current block
// height, then it will cancel the HTLC.
func TestChannelLinkExpiryTooSoonExitNode(t *testing.T) {
t.Parallel()
// The starting height for this test will be 200. So we'll base all
// HTLC starting points off of that.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
const startingHeight = 200
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, startingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
// We'll craft an HTLC packet, but set the final hop CLTV to 5 blocks
// after the current true height. This is less than the test invoice
// cltv delta of 6, so we expect the incoming htlc to be failed by the
// exit hop.
htlcAmt, totalTimelock, hops := generateHops(amount,
startingHeight-1, n.firstBobChannelLink)
// Now we'll send out the payment from Alice to Bob.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
// The payment should've failed as the time lock value was in the
// _past_.
if err == nil {
t.Fatalf("payment should have failed due to a too early " +
"time lock value")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T %v",
rtErr, err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailIncorrectDetails:
default:
t.Fatalf("expected incorrect_or_unknown_payment_details, "+
"instead have: %v", err)
}
}
// TestChannelLinkExpiryTooSoonExitNode tests that if we send a multi-hop HTLC,
// and the time lock is too early for an intermediate node, then they cancel
// the HTLC back to the sender.
func TestChannelLinkExpiryTooSoonMidNode(t *testing.T) {
t.Parallel()
// The starting height for this test will be 200. So we'll base all
// HTLC starting points off of that.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
const startingHeight = 200
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, startingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
// We'll craft an HTLC packet, but set the starting height to 3 blocks
// before the current true height. This means that the outgoing time
// lock of the middle hop will be at starting height + 3 blocks (channel
// policy time lock delta is 6 blocks). There is an expiry grace delta
// of 3 blocks relative to the current height, meaning that htlc will
// not be sent out by the middle hop.
htlcAmt, totalTimelock, hops := generateHops(amount,
startingHeight-3, n.firstBobChannelLink, n.carolChannelLink)
// Now we'll send out the payment from Alice to Bob.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
// The payment should've failed as the time lock value was in the
// _past_.
if err == nil {
t.Fatalf("payment should have failed due to a too early " +
"time lock value")
}
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected a ClearTextError, instead got: %T: %v",
rtErr, err)
}
switch rtErr.WireMessage().(type) {
case *lnwire.FailExpiryTooSoon:
default:
t.Fatalf("incorrect error, expected final time lock too "+
"early, instead have: %v", err)
}
}
// TestChannelLinkSingleHopMessageOrdering test checks ordering of message which
// flying around between Alice and Bob are correct when Bob sends payments to
// Alice.
func TestChannelLinkSingleHopMessageOrdering(t *testing.T) {
t.Parallel()
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
chanID := n.aliceChannelLink.ChanID()
messages := []expectedMessage{
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
{"alice", "bob", &lnwire.UpdateAddHTLC{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.UpdateFulfillHTLC{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
}
debug := false
if debug {
// Log message that alice receives.
n.aliceServer.intersect(createLogFunc("alice",
n.aliceChannelLink.ChanID()))
// Log message that bob receives.
n.bobServer.intersect(createLogFunc("bob",
n.firstBobChannelLink.ChanID()))
}
// Check that alice receives messages in right order.
n.aliceServer.intersect(createInterceptorFunc("[alice] <-- [bob]",
"alice", messages, chanID, false))
// Check that bob receives messages in right order.
n.bobServer.intersect(createInterceptorFunc("[alice] --> [bob]",
"bob", messages, chanID, false))
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink)
// Wait for:
// * HTLC add request to be sent to bob.
// * alice<->bob commitment state to be updated.
// * settle request to be sent back from bob to alice.
// * alice<->bob commitment state to be updated.
// * user notification to be sent.
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to make the payment: %v", err)
}
}
type mockPeer struct {
sync.Mutex
disconnected bool
sentMsgs chan lnwire.Message
quit chan struct{}
}
func (m *mockPeer) QuitSignal() <-chan struct{} {
return m.quit
}
var _ lnpeer.Peer = (*mockPeer)(nil)
func (m *mockPeer) SendMessage(sync bool, msgs ...lnwire.Message) error {
if m.disconnected {
return fmt.Errorf("disconnected")
}
select {
case m.sentMsgs <- msgs[0]:
case <-m.quit:
return fmt.Errorf("mockPeer shutting down")
}
return nil
}
func (m *mockPeer) SendMessageLazy(sync bool, msgs ...lnwire.Message) error {
return m.SendMessage(sync, msgs...)
}
func (m *mockPeer) AddNewChannel(_ *channeldb.OpenChannel,
_ <-chan struct{}) error {
return nil
}
func (m *mockPeer) WipeChannel(*wire.OutPoint) {}
func (m *mockPeer) PubKey() [33]byte {
return [33]byte{}
}
func (m *mockPeer) IdentityKey() *btcec.PublicKey {
return nil
}
func (m *mockPeer) Address() net.Addr {
return nil
}
func (m *mockPeer) LocalFeatures() *lnwire.FeatureVector {
return nil
}
func (m *mockPeer) RemoteFeatures() *lnwire.FeatureVector {
return nil
}
func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) (
ChannelLink, *lnwallet.LightningChannel, chan time.Time, func() error,
func(), func() (*lnwallet.LightningChannel, error), error) {
var chanIDBytes [8]byte
if _, err := io.ReadFull(rand.Reader, chanIDBytes[:]); err != nil {
return nil, nil, nil, nil, nil, nil, err
}
chanID := lnwire.NewShortChanIDFromInt(
binary.BigEndian.Uint64(chanIDBytes[:]))
aliceLc, bobLc, fCleanUp, err := createTestChannel(
alicePrivKey, bobPrivKey, chanAmt, chanAmt,
chanReserve, chanReserve, chanID,
)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
var (
decoder = newMockIteratorDecoder()
obfuscator = NewMockObfuscator()
alicePeer = &mockPeer{
sentMsgs: make(chan lnwire.Message, 2000),
quit: make(chan struct{}),
}
globalPolicy = ForwardingPolicy{
MinHTLCOut: lnwire.NewMSatFromSatoshis(5),
MaxHTLC: lnwire.NewMSatFromSatoshis(chanAmt),
BaseFee: lnwire.NewMSatFromSatoshis(1),
TimeLockDelta: 6,
}
invoiceRegistry = newMockRegistry(globalPolicy.TimeLockDelta)
)
pCache := newMockPreimageCache()
aliceDb := aliceLc.channel.State().Db
aliceSwitch, err := initSwitchWithDB(testStartingHeight, aliceDb)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
// Instantiate with a long interval, so that we can precisely control
// the firing via force feeding.
bticker := ticker.NewForce(time.Hour)
aliceCfg := ChannelLinkConfig{
FwrdingPolicy: globalPolicy,
Peer: alicePeer,
Switch: aliceSwitch,
Circuits: aliceSwitch.CircuitModifier(),
ForwardPackets: aliceSwitch.ForwardPackets,
DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
hop.ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
PreimageCache: pCache,
OnChannelFailure: func(lnwire.ChannelID,
lnwire.ShortChannelID, LinkFailureError) {
},
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
Registry: invoiceRegistry,
FeeEstimator: newMockFeeEstimator(),
ChainEvents: &contractcourt.ChainEventSubscription{},
BatchTicker: bticker,
FwdPkgGCTicker: ticker.NewForce(15 * time.Second),
PendingCommitTicker: ticker.New(time.Minute),
// Make the BatchSize and Min/MaxFeeUpdateTimeout large enough
// to not trigger commit updates automatically during tests.
BatchSize: 10000,
MinFeeUpdateTimeout: 30 * time.Minute,
MaxFeeUpdateTimeout: 40 * time.Minute,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
}
aliceLink := NewChannelLink(aliceCfg, aliceLc.channel)
start := func() error {
return aliceSwitch.AddLink(aliceLink)
}
go func() {
for {
select {
case <-aliceLink.(*channelLink).htlcUpdates:
case <-aliceLink.(*channelLink).quit:
return
}
}
}()
cleanUp := func() {
close(alicePeer.quit)
defer fCleanUp()
}
return aliceLink, bobLc.channel, bticker.Force, start, cleanUp,
aliceLc.restore, nil
}
func assertLinkBandwidth(t *testing.T, link ChannelLink,
expected lnwire.MilliSatoshi) {
currentBandwidth := link.Bandwidth()
_, _, line, _ := runtime.Caller(1)
if currentBandwidth != expected {
t.Fatalf("line %v: alice's link bandwidth is incorrect: "+
"expected %v, got %v", line, expected, currentBandwidth)
}
}
// handleStateUpdate handles the messages sent from the link after
// the batch ticker has triggered a state update.
func handleStateUpdate(link *channelLink,
remoteChannel *lnwallet.LightningChannel) error {
sentMsgs := link.cfg.Peer.(*mockPeer).sentMsgs
var msg lnwire.Message
select {
case msg = <-sentMsgs:
case <-time.After(60 * time.Second):
return fmt.Errorf("did not receive CommitSig from Alice")
}
// The link should be sending a commit sig at this point.
commitSig, ok := msg.(*lnwire.CommitSig)
if !ok {
return fmt.Errorf("expected CommitSig, got %T", msg)
}
// Let the remote channel receive the commit sig, and
// respond with a revocation + commitsig.
err := remoteChannel.ReceiveNewCommitment(
commitSig.CommitSig, commitSig.HtlcSigs)
if err != nil {
return err
}
remoteRev, _, err := remoteChannel.RevokeCurrentCommitment()
if err != nil {
return err
}
link.HandleChannelUpdate(remoteRev)
remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
if err != nil {
return err
}
commitSig = &lnwire.CommitSig{
CommitSig: remoteSig,
HtlcSigs: remoteHtlcSigs,
}
link.HandleChannelUpdate(commitSig)
// This should make the link respond with a revocation.
select {
case msg = <-sentMsgs:
case <-time.After(60 * time.Second):
return fmt.Errorf("did not receive RevokeAndAck from Alice")
}
revoke, ok := msg.(*lnwire.RevokeAndAck)
if !ok {
return fmt.Errorf("expected RevokeAndAck got %T", msg)
}
_, _, _, _, err = remoteChannel.ReceiveRevocation(revoke)
if err != nil {
return fmt.Errorf("unable to receive "+
"revocation: %v", err)
}
return nil
}
// updateState is used exchange the messages necessary to do a full state
// transition. If initiateUpdate=true, then this call will make the link
// trigger an update by sending on the batchTick channel, if not, it will
// make the remoteChannel initiate the state update.
func updateState(batchTick chan time.Time, link *channelLink,
remoteChannel *lnwallet.LightningChannel,
initiateUpdate bool) error {
sentMsgs := link.cfg.Peer.(*mockPeer).sentMsgs
if initiateUpdate {
// Trigger update by ticking the batchTicker.
select {
case batchTick <- time.Now():
case <-link.quit:
return fmt.Errorf("link shutting down")
}
return handleStateUpdate(link, remoteChannel)
}
// The remote is triggering the state update, emulate this by
// signing and sending CommitSig to the link.
remoteSig, remoteHtlcSigs, _, err := remoteChannel.SignNextCommitment()
if err != nil {
return err
}
commitSig := &lnwire.CommitSig{
CommitSig: remoteSig,
HtlcSigs: remoteHtlcSigs,
}
link.HandleChannelUpdate(commitSig)
// The link should respond with a revocation + commit sig.
var msg lnwire.Message
select {
case msg = <-sentMsgs:
case <-time.After(60 * time.Second):
return fmt.Errorf("did not receive RevokeAndAck from Alice")
}
revoke, ok := msg.(*lnwire.RevokeAndAck)
if !ok {
return fmt.Errorf("expected RevokeAndAck got %T",
msg)
}
_, _, _, _, err = remoteChannel.ReceiveRevocation(revoke)
if err != nil {
return fmt.Errorf("unable to receive "+
"revocation: %v", err)
}
select {
case msg = <-sentMsgs:
case <-time.After(60 * time.Second):
return fmt.Errorf("did not receive CommitSig from Alice")
}
commitSig, ok = msg.(*lnwire.CommitSig)
if !ok {
return fmt.Errorf("expected CommitSig, got %T", msg)
}
err = remoteChannel.ReceiveNewCommitment(
commitSig.CommitSig, commitSig.HtlcSigs)
if err != nil {
return err
}
// Lastly, send a revocation back to the link.
remoteRev, _, err := remoteChannel.RevokeCurrentCommitment()
if err != nil {
return err
}
link.HandleChannelUpdate(remoteRev)
// Sleep to make sure Alice has handled the remote revocation.
time.Sleep(500 * time.Millisecond)
return nil
}
// TestChannelLinkBandwidthConsistency ensures that the reported bandwidth of a
// given ChannelLink is properly updated in response to downstream messages
// from the switch, and upstream messages from its channel peer.
//
// TODO(roasbeef): add sync hook into packet processing so can eliminate all
// sleep in this test and the one below
func TestChannelLinkBandwidthConsistency(t *testing.T) {
if !build.IsDevBuild() {
t.Fatalf("htlcswitch tests must be run with '-tags debug")
}
t.Parallel()
// TODO(roasbeef): replace manual bit twiddling with concept of
// resource cost for packets?
// * or also able to consult link
// We'll start the test by creating a single instance of
const chanAmt = btcutil.SatoshiPerBitcoin * 5
aliceLink, bobChannel, tmr, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
carolChanID = lnwire.NewShortChanIDFromInt(3)
mockBlob [lnwire.OnionPacketSize]byte
coreChan = aliceLink.(*channelLink).channel
coreLink = aliceLink.(*channelLink)
defaultCommitFee = coreChan.StateSnapshot().CommitFee
aliceStartingBandwidth = aliceLink.Bandwidth()
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
// We put Alice into hodl.ExitSettle mode, such that she won't settle
// incoming HTLCs automatically.
coreLink.cfg.HodlMask = hodl.MaskFromFlags(hodl.ExitSettle)
estimator := chainfee.NewStaticEstimator(6000, 0)
feePerKw, err := estimator.EstimateFeePerKW(1)
if err != nil {
t.Fatalf("unable to query fee estimator: %v", err)
}
htlcFee := lnwire.NewMSatFromSatoshis(
feePerKw.FeeForWeight(input.HTLCWeight),
)
// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the
// commitment fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, aliceLink, expectedBandwidth)
// Next, we'll create an HTLC worth 1 BTC, and send it into the link as
// a switch initiated payment. The resulting bandwidth should
// now be decremented to reflect the new HTLC.
htlcAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
invoice, htlc, _, err := generatePayment(
htlcAmt, htlcAmt, 5, mockBlob,
)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
addPkt := htlcPacket{
htlc: htlc,
incomingChanID: hop.Source,
incomingHTLCID: 0,
obfuscator: NewMockObfuscator(),
}
circuit := makePaymentCircuit(&htlc.PaymentHash, &addPkt)
_, err = coreLink.cfg.Switch.commitCircuits(&circuit)
if err != nil {
t.Fatalf("unable to commit circuit: %v", err)
}
addPkt.circuit = &circuit
if err := aliceLink.HandleSwitchPacket(&addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
time.Sleep(time.Millisecond * 500)
// The resulting bandwidth should reflect that Alice is paying the
// htlc amount in addition to the htlc fee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// Alice should send the HTLC to Bob.
var msg lnwire.Message
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
addHtlc, ok := msg.(*lnwire.UpdateAddHTLC)
if !ok {
t.Fatalf("expected UpdateAddHTLC, got %T", msg)
}
bobIndex, err := bobChannel.ReceiveHTLC(addHtlc)
if err != nil {
t.Fatalf("bob failed receiving htlc: %v", err)
}
// Lock in the HTLC.
if err := updateState(tmr, coreLink, bobChannel, true); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// Locking in the HTLC should not change Alice's bandwidth.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// If we now send in a valid HTLC settle for the prior HTLC we added,
// then the bandwidth should remain unchanged as the remote party will
// gain additional channel balance.
err = bobChannel.SettleHTLC(*invoice.Terms.PaymentPreimage, bobIndex, nil, nil, nil)
if err != nil {
t.Fatalf("unable to settle htlc: %v", err)
}
htlcSettle := &lnwire.UpdateFulfillHTLC{
ID: 0,
PaymentPreimage: *invoice.Terms.PaymentPreimage,
}
aliceLink.HandleChannelUpdate(htlcSettle)
time.Sleep(time.Millisecond * 500)
// Since the settle is not locked in yet, Alice's bandwidth should still
// reflect that she has to pay the fee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// Lock in the settle.
if err := updateState(tmr, coreLink, bobChannel, false); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// Now that it is settled, Alice should have gotten the htlc fee back.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt)
// Next, we'll add another HTLC initiated by the switch (of the same
// amount as the prior one).
_, htlc, _, err = generatePayment(htlcAmt, htlcAmt, 5, mockBlob)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
addPkt = htlcPacket{
htlc: htlc,
incomingChanID: hop.Source,
incomingHTLCID: 1,
obfuscator: NewMockObfuscator(),
}
circuit = makePaymentCircuit(&htlc.PaymentHash, &addPkt)
_, err = coreLink.cfg.Switch.commitCircuits(&circuit)
if err != nil {
t.Fatalf("unable to commit circuit: %v", err)
}
addPkt.circuit = &circuit
if err := aliceLink.HandleSwitchPacket(&addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
time.Sleep(time.Millisecond * 500)
// Again, Alice's bandwidth decreases by htlcAmt+htlcFee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-2*htlcAmt-htlcFee)
// Alice will send the HTLC to Bob.
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
addHtlc, ok = msg.(*lnwire.UpdateAddHTLC)
if !ok {
t.Fatalf("expected UpdateAddHTLC, got %T", msg)
}
bobIndex, err = bobChannel.ReceiveHTLC(addHtlc)
if err != nil {
t.Fatalf("bob failed receiving htlc: %v", err)
}
// Lock in the HTLC, which should not affect the bandwidth.
if err := updateState(tmr, coreLink, bobChannel, true); err != nil {
t.Fatalf("unable to update state: %v", err)
}
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt*2-htlcFee)
// With that processed, we'll now generate an HTLC fail (sent by the
// remote peer) to cancel the HTLC we just added. This should return us
// back to the bandwidth of the link right before the HTLC was sent.
err = bobChannel.FailHTLC(bobIndex, []byte("nop"), nil, nil, nil)
if err != nil {
t.Fatalf("unable to fail htlc: %v", err)
}
failMsg := &lnwire.UpdateFailHTLC{
ID: 1,
Reason: lnwire.OpaqueReason([]byte("nop")),
}
aliceLink.HandleChannelUpdate(failMsg)
time.Sleep(time.Millisecond * 500)
// Before the Fail gets locked in, the bandwidth should remain unchanged.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt*2-htlcFee)
// Lock in the Fail.
if err := updateState(tmr, coreLink, bobChannel, false); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// Now the bandwidth should reflect the failed HTLC.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt)
// Moving along, we'll now receive a new HTLC from the remote peer,
// with an ID of 0 as this is their first HTLC. The bandwidth should
// remain unchanged (but Alice will need to pay the fee for the extra
// HTLC).
htlcAmt, totalTimelock, hops := generateHops(htlcAmt, testStartingHeight,
coreLink)
blob, err := generateRoute(hops...)
if err != nil {
t.Fatalf("unable to gen route: %v", err)
}
invoice, htlc, _, err = generatePayment(
htlcAmt, htlcAmt, totalTimelock, blob,
)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
// We must add the invoice to the registry, such that Alice expects
// this payment.
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
*invoice, htlc.PaymentHash,
)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
htlc.ID = 0
_, err = bobChannel.AddHTLC(htlc, nil)
if err != nil {
t.Fatalf("unable to add htlc: %v", err)
}
aliceLink.HandleChannelUpdate(htlc)
// Alice's balance remains unchanged until this HTLC is locked in.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt)
// Lock in the HTLC.
if err := updateState(tmr, coreLink, bobChannel, false); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// Since Bob is adding this HTLC, Alice only needs to pay the fee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
time.Sleep(time.Millisecond * 500)
addPkt = htlcPacket{
htlc: htlc,
incomingChanID: aliceLink.ShortChanID(),
incomingHTLCID: 0,
obfuscator: NewMockObfuscator(),
}
circuit = makePaymentCircuit(&htlc.PaymentHash, &addPkt)
_, err = coreLink.cfg.Switch.commitCircuits(&circuit)
if err != nil {
t.Fatalf("unable to commit circuit: %v", err)
}
addPkt.outgoingChanID = carolChanID
addPkt.outgoingHTLCID = 0
err = coreLink.cfg.Switch.openCircuits(addPkt.keystone())
if err != nil {
t.Fatalf("unable to set keystone: %v", err)
}
// Next, we'll settle the HTLC with our knowledge of the pre-image that
// we eventually learn (simulating a multi-hop payment). The bandwidth
// of the channel should now be re-balanced to the starting point.
settlePkt := htlcPacket{
incomingChanID: aliceLink.ShortChanID(),
incomingHTLCID: 0,
circuit: &circuit,
outgoingChanID: addPkt.outgoingChanID,
outgoingHTLCID: addPkt.outgoingHTLCID,
htlc: &lnwire.UpdateFulfillHTLC{
ID: 0,
PaymentPreimage: *invoice.Terms.PaymentPreimage,
},
obfuscator: NewMockObfuscator(),
}
if err := aliceLink.HandleSwitchPacket(&settlePkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
time.Sleep(time.Millisecond * 500)
// Settling this HTLC gives Alice all her original bandwidth back.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth)
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
settleMsg, ok := msg.(*lnwire.UpdateFulfillHTLC)
if !ok {
t.Fatalf("expected UpdateFulfillHTLC, got %T", msg)
}
err = bobChannel.ReceiveHTLCSettle(settleMsg.PaymentPreimage, settleMsg.ID)
if err != nil {
t.Fatalf("failed receiving fail htlc: %v", err)
}
// After failing an HTLC, the link will automatically trigger
// a state update.
if err := handleStateUpdate(coreLink, bobChannel); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// Finally, we'll test the scenario of failing an HTLC received by the
// remote node. This should result in no perceived bandwidth changes.
htlcAmt, totalTimelock, hops = generateHops(htlcAmt, testStartingHeight,
coreLink)
blob, err = generateRoute(hops...)
if err != nil {
t.Fatalf("unable to gen route: %v", err)
}
invoice, htlc, _, err = generatePayment(
htlcAmt, htlcAmt, totalTimelock, blob,
)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
err = coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
*invoice, htlc.PaymentHash,
)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
// Since we are not using the link to handle HTLC IDs for the
// remote channel, we must set this manually. This is the second
// HTLC we add, hence it should have an ID of 1 (Alice's channel
// link will set this automatically for her side).
htlc.ID = 1
_, err = bobChannel.AddHTLC(htlc, nil)
if err != nil {
t.Fatalf("unable to add htlc: %v", err)
}
aliceLink.HandleChannelUpdate(htlc)
time.Sleep(time.Millisecond * 500)
// No changes before the HTLC is locked in.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth)
if err := updateState(tmr, coreLink, bobChannel, false); err != nil {
t.Fatalf("unable to update state: %v", err)
}
// After lock-in, Alice will have to pay the htlc fee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcFee)
addPkt = htlcPacket{
htlc: htlc,
incomingChanID: aliceLink.ShortChanID(),
incomingHTLCID: 1,
obfuscator: NewMockObfuscator(),
}
circuit = makePaymentCircuit(&htlc.PaymentHash, &addPkt)
_, err = coreLink.cfg.Switch.commitCircuits(&circuit)
if err != nil {
t.Fatalf("unable to commit circuit: %v", err)
}
addPkt.outgoingChanID = carolChanID
addPkt.outgoingHTLCID = 1
err = coreLink.cfg.Switch.openCircuits(addPkt.keystone())
if err != nil {
t.Fatalf("unable to set keystone: %v", err)
}
failPkt := htlcPacket{
incomingChanID: aliceLink.ShortChanID(),
incomingHTLCID: 1,
circuit: &circuit,
outgoingChanID: addPkt.outgoingChanID,
outgoingHTLCID: addPkt.outgoingHTLCID,
htlc: &lnwire.UpdateFailHTLC{
ID: 1,
},
obfuscator: NewMockObfuscator(),
}
if err := aliceLink.HandleSwitchPacket(&failPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
time.Sleep(time.Millisecond * 500)
// Alice should get all her bandwidth back.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth)
// Message should be sent to Bob.
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
failMsg, ok = msg.(*lnwire.UpdateFailHTLC)
if !ok {
t.Fatalf("expected UpdateFailHTLC, got %T", msg)
}
err = bobChannel.ReceiveFailHTLC(failMsg.ID, []byte("fail"))
if err != nil {
t.Fatalf("failed receiving fail htlc: %v", err)
}
// After failing an HTLC, the link will automatically trigger
// a state update.
if err := handleStateUpdate(coreLink, bobChannel); err != nil {
t.Fatalf("unable to update state: %v", err)
}
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth)
}
// genAddsAndCircuits creates `numHtlcs` sequential ADD packets and there
// corresponding circuits. The provided `htlc` is used in all test packets.
func genAddsAndCircuits(numHtlcs int, htlc *lnwire.UpdateAddHTLC) (
[]*htlcPacket, []*PaymentCircuit) {
addPkts := make([]*htlcPacket, 0, numHtlcs)
circuits := make([]*PaymentCircuit, 0, numHtlcs)
for i := 0; i < numHtlcs; i++ {
addPkt := htlcPacket{
htlc: htlc,
incomingChanID: hop.Source,
incomingHTLCID: uint64(i),
obfuscator: NewMockObfuscator(),
}
circuit := makePaymentCircuit(&htlc.PaymentHash, &addPkt)
addPkt.circuit = &circuit
addPkts = append(addPkts, &addPkt)
circuits = append(circuits, &circuit)
}
return addPkts, circuits
}
// TestChannelLinkTrimCircuitsPending checks that the switch and link properly
// trim circuits if there are open circuits corresponding to ADDs on a pending
// commmitment transaction.
func TestChannelLinkTrimCircuitsPending(t *testing.T) {
t.Parallel()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
numHtlcs = 4
halfHtlcs = numHtlcs / 2
)
// We'll start by creating a new link with our chanAmt (5 BTC). We will
// only be testing Alice's behavior, so the reference to Bob's channel
// state is unnecessary.
aliceLink, _, batchTicker, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
alice := newPersistentLinkHarness(
t, aliceLink, batchTicker, restore,
)
// Compute the static fees that will be used to determine the
// correctness of Alice's bandwidth when forwarding HTLCs.
estimator := chainfee.NewStaticEstimator(6000, 0)
feePerKw, err := estimator.EstimateFeePerKW(1)
if err != nil {
t.Fatalf("unable to query fee estimator: %v", err)
}
defaultCommitFee := alice.channel.StateSnapshot().CommitFee
htlcFee := lnwire.NewMSatFromSatoshis(
feePerKw.FeeForWeight(input.HTLCWeight),
)
// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the commitment
// fee and fee of adding an HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, alice.link, expectedBandwidth)
// Capture Alice's starting bandwidth to perform later, relative
// bandwidth assertions.
aliceStartingBandwidth := alice.link.Bandwidth()
// Next, we'll create an HTLC worth 1 BTC that will be used as a dummy
// message for the test.
var mockBlob [lnwire.OnionPacketSize]byte
htlcAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
_, htlc, _, err := generatePayment(htlcAmt, htlcAmt, 5, mockBlob)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
// Create `numHtlc` htlcPackets and payment circuits that will be used
// to drive the test. All of the packets will use the same dummy HTLC.
addPkts, circuits := genAddsAndCircuits(numHtlcs, htlc)
// To begin the test, start by committing the circuits belong to our
// first two HTLCs.
fwdActions := alice.commitCircuits(circuits[:halfHtlcs])
// Both of these circuits should have successfully added, as this is the
// first attempt to send them.
if len(fwdActions.Adds) != halfHtlcs {
t.Fatalf("expected %d circuits to be added", halfHtlcs)
}
alice.assertNumPendingNumOpenCircuits(2, 0)
// Since both were committed successfully, we will now deliver them to
// Alice's link.
for _, addPkt := range addPkts[:halfHtlcs] {
if err := alice.link.HandleSwitchPacket(addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
}
// Wait until Alice's link has sent both HTLCs via the peer.
alice.checkSent(addPkts[:halfHtlcs])
// The resulting bandwidth should reflect that Alice is paying both
// htlc amounts, in addition to both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
// Now, initiate a state transition by Alice so that the pending HTLCs
// are locked in. This will *not* involve any participation by Bob,
// which ensures the commitment will remain in a pending state.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Restart Alice's link, which simulates a disconnection with the remote
// peer.
cleanUp = alice.restart(false, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Make a second attempt to commit the first two circuits. This can
// happen if the incoming link flaps, but also allows us to verify that
// the circuits were trimmed properly.
fwdActions = alice.commitCircuits(circuits[:halfHtlcs])
// Since Alice has a pending commitment with the first two HTLCs, the
// restart should not have trimmed them from the circuit map.
// Therefore, we expect both of these circuits to be dropped by the
// switch, as keystones should still be set.
if len(fwdActions.Drops) != halfHtlcs {
t.Fatalf("expected %d packets to be dropped", halfHtlcs)
}
// The resulting bandwidth should remain unchanged from before,
// reflecting that Alice is paying both htlc amounts, in addition to
// both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
// Now, restart Alice's link *and* the entire switch. This will ensure
// that entire circuit map is reloaded from disk, and we can now test
// against the behavioral differences of committing circuits that
// conflict with duplicate circuits after a restart.
cleanUp = alice.restart(true, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Alice should not send out any messages. Even though Alice has a
// pending commitment transaction, channel reestablishment is not
// enabled in this test.
select {
case <-alice.msgs:
t.Fatalf("message should not have been sent by Alice")
case <-time.After(time.Second):
}
// We will now try to commit the circuits for all of our HTLCs. The
// first two are already on the pending commitment transaction, the
// latter two are new HTLCs.
fwdActions = alice.commitCircuits(circuits)
// The first two circuits should have been dropped, as they are still on
// the pending commitment transaction, and the restart should not have
// trimmed the circuits for these valid HTLCs.
if len(fwdActions.Drops) != halfHtlcs {
t.Fatalf("expected %d packets to be dropped", halfHtlcs)
}
// The latter two circuits are unknown the circuit map, and should
// report being added.
if len(fwdActions.Adds) != halfHtlcs {
t.Fatalf("expected %d packets to be added", halfHtlcs)
}
// Deliver the latter two HTLCs to Alice's links so that they can be
// processed and added to the in-memory commitment state.
for _, addPkt := range addPkts[halfHtlcs:] {
if err := alice.link.HandleSwitchPacket(addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
}
// Wait for Alice to send the two latter HTLCs via the peer.
alice.checkSent(addPkts[halfHtlcs:])
// With two HTLCs on the pending commit, and two added to the in-memory
// commitment state, the resulting bandwidth should reflect that Alice
// is paying the all htlc amounts in addition to all htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-numHtlcs*(htlcAmt+htlcFee),
)
// We will try to initiate a state transition for Alice, which will
// ensure the circuits for the two in-memory HTLCs are opened. However,
// since we have a pending commitment, these HTLCs will not actually be
// included in a commitment.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(4, 4)
// Restart Alice's link to simulate a disconnect. Since the switch
// remains up throughout, the two latter HTLCs will remain in the link's
// mailbox, and will reprocessed upon being reattached to the link.
cleanUp = alice.restart(false, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(4, 2)
// Again, try to recommit all of our circuits.
fwdActions = alice.commitCircuits(circuits)
// It is expected that all of these will get dropped by the switch.
// The first two circuits are still open as a result of being on the
// commitment transaction. The latter two should have had their open
// circuits trimmed, *but* since the HTLCs are still in Alice's mailbox,
// the switch knows not to fail them as a result of the latter two
// circuits never having been loaded from disk.
if len(fwdActions.Drops) != numHtlcs {
t.Fatalf("expected %d packets to be dropped", numHtlcs)
}
// Wait for the latter two htlcs to be pulled from the mailbox, added to
// the in-memory channel state, and sent out via the peer.
alice.checkSent(addPkts[halfHtlcs:])
// This should result in reconstructing the same bandwidth as our last
// assertion. There are two HTLCs on the pending commit, and two added
// to the in-memory commitment state, the resulting bandwidth should
// reflect that Alice is paying the all htlc amounts in addition to all
// htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-numHtlcs*(htlcAmt+htlcFee),
)
// Again, we will try to initiate a state transition for Alice, which
// will ensure the circuits for the two in-memory HTLCs are opened.
// As before, these HTLCs will not actually be included in a commitment
// since we have a pending commitment.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(4, 4)
// As a final persistence check, we will restart the link and switch,
// wiping the latter two HTLCs from memory, and forcing their circuits
// to be reloaded from disk.
cleanUp = alice.restart(true, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(4, 2)
// Alice's mailbox will be empty after the restart, and no channel
// reestablishment is configured, so no messages will be sent upon
// restart.
select {
case <-alice.msgs:
t.Fatalf("message should not have been sent by Alice")
case <-time.After(time.Second):
}
// Finally, make one last attempt to commit all circuits.
fwdActions = alice.commitCircuits(circuits)
// The first two HTLCs should still be dropped by the htlcswitch. Their
// existence on the pending commitment transaction should prevent their
// open circuits from being trimmed.
if len(fwdActions.Drops) != halfHtlcs {
t.Fatalf("expected %d packets to be dropped", halfHtlcs)
}
// The latter two HTLCs should now be failed by the switch. These will
// have been trimmed by the link or switch restarting, and since the
// HTLCs are known to be lost from memory (since their circuits were
// loaded from disk), it is safe fail them back as they won't ever be
// delivered to the outgoing link.
if len(fwdActions.Fails) != halfHtlcs {
t.Fatalf("expected %d packets to be dropped", halfHtlcs)
}
// Since the latter two HTLCs have been completely dropped from memory,
// only the first two HTLCs we added should still be reflected in the
// channel bandwidth.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
}
// TestChannelLinkTrimCircuitsNoCommit checks that the switch and link properly trim
// circuits if the ADDs corresponding to open circuits are never committed.
func TestChannelLinkTrimCircuitsNoCommit(t *testing.T) {
if !build.IsDevBuild() {
t.Fatalf("htlcswitch tests must be run with '-tags debug")
}
t.Parallel()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
numHtlcs = 4
halfHtlcs = numHtlcs / 2
)
// We'll start by creating a new link with our chanAmt (5 BTC). We will
// only be testing Alice's behavior, so the reference to Bob's channel
// state is unnecessary.
aliceLink, _, batchTicker, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
alice := newPersistentLinkHarness(
t, aliceLink, batchTicker, restore,
)
// We'll put Alice into hodl.Commit mode, such that the circuits for any
// outgoing ADDs are opened, but the changes are not committed in the
// channel state.
alice.coreLink.cfg.HodlMask = hodl.Commit.Mask()
// Compute the static fees that will be used to determine the
// correctness of Alice's bandwidth when forwarding HTLCs.
estimator := chainfee.NewStaticEstimator(6000, 0)
feePerKw, err := estimator.EstimateFeePerKW(1)
if err != nil {
t.Fatalf("unable to query fee estimator: %v", err)
}
defaultCommitFee := alice.channel.StateSnapshot().CommitFee
htlcFee := lnwire.NewMSatFromSatoshis(
feePerKw.FeeForWeight(input.HTLCWeight),
)
// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the commitment
// fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, alice.link, expectedBandwidth)
// Capture Alice's starting bandwidth to perform later, relative
// bandwidth assertions.
aliceStartingBandwidth := alice.link.Bandwidth()
// Next, we'll create an HTLC worth 1 BTC that will be used as a dummy
// message for the test.
var mockBlob [lnwire.OnionPacketSize]byte
htlcAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
_, htlc, _, err := generatePayment(htlcAmt, htlcAmt, 5, mockBlob)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
// Create `numHtlc` htlcPackets and payment circuits that will be used
// to drive the test. All of the packets will use the same dummy HTLC.
addPkts, circuits := genAddsAndCircuits(numHtlcs, htlc)
// To begin the test, start by committing the circuits belong to our
// first two HTLCs.
fwdActions := alice.commitCircuits(circuits[:halfHtlcs])
// Both of these circuits should have successfully added, as this is the
// first attempt to send them.
if len(fwdActions.Adds) != halfHtlcs {
t.Fatalf("expected %d circuits to be added", halfHtlcs)
}
// Since both were committed successfully, we will now deliver them to
// Alice's link.
for _, addPkt := range addPkts[:halfHtlcs] {
if err := alice.link.HandleSwitchPacket(addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
}
// Wait until Alice's link has sent both HTLCs via the peer.
alice.checkSent(addPkts[:halfHtlcs])
// The resulting bandwidth should reflect that Alice is paying both
// htlc amounts, in addition to both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
alice.assertNumPendingNumOpenCircuits(2, 0)
// Now, init a state transition by Alice to try and commit the HTLCs.
// Since she is in hodl.Commit mode, this will fail, but the circuits
// will be opened persistently.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Restart Alice's link, which simulates a disconnection with the remote
// peer. Alice's link and switch should trim the circuits that were
// opened but not committed.
cleanUp = alice.restart(false, false, hodl.Commit)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 0)
// The first two HTLCs should have been reset in Alice's mailbox since
// the switch was not shutdown. Knowing this the switch should drop the
// two circuits, even if the circuits were trimmed.
fwdActions = alice.commitCircuits(circuits[:halfHtlcs])
if len(fwdActions.Drops) != halfHtlcs {
t.Fatalf("expected %d packets to be dropped since "+
"the switch has not been restarted", halfHtlcs)
}
// Wait for alice to process the first two HTLCs resend them via the
// peer.
alice.checkSent(addPkts[:halfHtlcs])
// The resulting bandwidth should reflect that Alice is paying both htlc
// amounts, in addition to both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
// Again, initiate another state transition by Alice to try and commit
// the HTLCs. Since she is in hodl.Commit mode, this will fail, but the
// circuits will be opened persistently.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Now, we we will do a full restart of the link and switch, configuring
// Alice again in hodl.Commit mode. Since none of the HTLCs were
// actually committed, the previously opened circuits should be trimmed
// by both the link and switch.
cleanUp = alice.restart(true, false, hodl.Commit)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 0)
// Attempt another commit of our first two circuits. Both should fail,
// as the opened circuits should have been trimmed, and circuit map
// recognizes that these HTLCs were lost during the restart.
fwdActions = alice.commitCircuits(circuits[:halfHtlcs])
if len(fwdActions.Fails) != halfHtlcs {
t.Fatalf("expected %d packets to be failed", halfHtlcs)
}
// Bob should not receive any HTLCs from Alice, since Alice's mailbox is
// empty and there is no pending commitment.
select {
case <-alice.msgs:
t.Fatalf("received unexpected message from Alice")
case <-time.After(time.Second):
}
// Alice's bandwidth should have reverted back to her starting value.
assertLinkBandwidth(t, alice.link, aliceStartingBandwidth)
// Now, try to commit the last two payment circuits, which are unused
// thus far. These should succeed without hesitation.
fwdActions = alice.commitCircuits(circuits[halfHtlcs:])
if len(fwdActions.Adds) != halfHtlcs {
t.Fatalf("expected %d packets to be added", halfHtlcs)
}
// Deliver the last two HTLCs to the link via Alice's mailbox.
for _, addPkt := range addPkts[halfHtlcs:] {
if err := alice.link.HandleSwitchPacket(addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
}
// Verify that Alice processed and sent out the ADD packets via the
// peer.
alice.checkSent(addPkts[halfHtlcs:])
// The resulting bandwidth should reflect that Alice is paying both htlc
// amounts, in addition to both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
// Now, initiate a state transition for Alice. Since we are hodl.Commit
// mode, this will only open the circuits that were added to the
// in-memory channel state.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(4, 2)
// Restart Alice's link, and place her back in hodl.Commit mode. On
// restart, all previously opened circuits should be trimmed by both the
// link and the switch.
cleanUp = alice.restart(false, false, hodl.Commit)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(4, 0)
// Now, try to commit all of known circuits.
fwdActions = alice.commitCircuits(circuits)
// The first two HTLCs will fail to commit for the same reason as
// before, the circuits have been trimmed.
if len(fwdActions.Fails) != halfHtlcs {
t.Fatalf("expected %d packet to be failed", halfHtlcs)
}
// The last two HTLCs will be dropped, as thought the circuits are
// trimmed, the switch is aware that the HTLCs are still in Alice's
// mailbox.
if len(fwdActions.Drops) != halfHtlcs {
t.Fatalf("expected %d packet to be dropped", halfHtlcs)
}
// Wait until Alice reprocesses the last two HTLCs and sends them via
// the peer.
alice.checkSent(addPkts[halfHtlcs:])
// Her bandwidth should now reflect having sent only those two HTLCs.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-halfHtlcs*(htlcAmt+htlcFee),
)
// Now, initiate a state transition for Alice. Since we are hodl.Commit
// mode, this will only open the circuits that were added to the
// in-memory channel state.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(4, 2)
// Finally, do one last restart of both the link and switch. This will
// flush the HTLCs from the mailbox. The circuits should now be trimmed
// for all of the HTLCs.
cleanUp = alice.restart(true, false, hodl.Commit)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(4, 0)
// Bob should not receive any HTLCs from Alice, as none of the HTLCs are
// in Alice's mailbox, and channel reestablishment is disabled.
select {
case <-alice.msgs:
t.Fatalf("received unexpected message from Alice")
case <-time.After(time.Second):
}
// Attempt to commit the last two circuits, both should now fail since
// though they were opened before shutting down, the circuits have been
// properly trimmed.
fwdActions = alice.commitCircuits(circuits[halfHtlcs:])
if len(fwdActions.Fails) != halfHtlcs {
t.Fatalf("expected %d packet to be failed", halfHtlcs)
}
// Alice balance should not have changed since the start.
assertLinkBandwidth(t, alice.link, aliceStartingBandwidth)
}
// TestChannelLinkTrimCircuitsRemoteCommit checks that the switch and link
// don't trim circuits if the ADD is locked in on the remote commitment but
// not on our local commitment.
func TestChannelLinkTrimCircuitsRemoteCommit(t *testing.T) {
t.Parallel()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
numHtlcs = 2
)
// We'll start by creating a new link with our chanAmt (5 BTC).
aliceLink, bobChan, batchTicker, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
defer cleanUp()
alice := newPersistentLinkHarness(
t, aliceLink, batchTicker, restore,
)
// Compute the static fees that will be used to determine the
// correctness of Alice's bandwidth when forwarding HTLCs.
estimator := chainfee.NewStaticEstimator(6000, 0)
feePerKw, err := estimator.EstimateFeePerKW(1)
if err != nil {
t.Fatalf("unable to query fee estimator: %v", err)
}
defaultCommitFee := alice.channel.StateSnapshot().CommitFee
htlcFee := lnwire.NewMSatFromSatoshis(
feePerKw.FeeForWeight(input.HTLCWeight),
)
// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the commitment
// fee and fee of adding an HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, alice.link, expectedBandwidth)
// Capture Alice's starting bandwidth to perform later, relative
// bandwidth assertions.
aliceStartingBandwidth := alice.link.Bandwidth()
// Next, we'll create an HTLC worth 1 BTC that will be used as a dummy
// message for the test.
var mockBlob [lnwire.OnionPacketSize]byte
htlcAmt := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
_, htlc, _, err := generatePayment(htlcAmt, htlcAmt, 5, mockBlob)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
// Create `numHtlc` htlcPackets and payment circuits that will be used
// to drive the test. All of the packets will use the same dummy HTLC.
addPkts, circuits := genAddsAndCircuits(numHtlcs, htlc)
// To begin the test, start by committing the circuits for our first two
// HTLCs.
fwdActions := alice.commitCircuits(circuits)
// Both of these circuits should have successfully added, as this is the
// first attempt to send them.
if len(fwdActions.Adds) != numHtlcs {
t.Fatalf("expected %d circuits to be added", numHtlcs)
}
alice.assertNumPendingNumOpenCircuits(2, 0)
// Since both were committed successfully, we will now deliver them to
// Alice's link.
for _, addPkt := range addPkts {
if err := alice.link.HandleSwitchPacket(addPkt); err != nil {
t.Fatalf("unable to handle switch packet: %v", err)
}
}
// Wait until Alice's link has sent both HTLCs via the peer.
alice.checkSent(addPkts)
// Pass both of the htlcs to Bob.
for i, addPkt := range addPkts {
pkt, ok := addPkt.htlc.(*lnwire.UpdateAddHTLC)
if !ok {
t.Fatalf("unable to add packet")
}
pkt.ID = uint64(i)
_, err := bobChan.ReceiveHTLC(pkt)
if err != nil {
t.Fatalf("unable to receive htlc: %v", err)
}
}
// The resulting bandwidth should reflect that Alice is paying both
// htlc amounts, in addition to both htlc fees.
assertLinkBandwidth(t, alice.link,
aliceStartingBandwidth-numHtlcs*(htlcAmt+htlcFee),
)
// Now, initiate a state transition by Alice so that the pending HTLCs
// are locked in.
alice.trySignNextCommitment()
alice.assertNumPendingNumOpenCircuits(2, 2)
select {
case aliceMsg := <-alice.msgs:
// Pass the commitment signature to Bob.
sig, ok := aliceMsg.(*lnwire.CommitSig)
if !ok {
t.Fatalf("alice did not send commitment signature")
}
err := bobChan.ReceiveNewCommitment(sig.CommitSig, sig.HtlcSigs)
if err != nil {
t.Fatalf("unable to receive new commitment: %v", err)
}
case <-time.After(time.Second):
}
// Next, revoke Bob's current commitment and send it to Alice so that we
// can test that Alice's circuits aren't trimmed.
rev, _, err := bobChan.RevokeCurrentCommitment()
if err != nil {
t.Fatalf("unable to revoke current commitment: %v", err)
}
_, _, _, _, err = alice.channel.ReceiveRevocation(rev)
if err != nil {
t.Fatalf("unable to receive revocation: %v", err)
}
// Restart Alice's link, which simulates a disconnection with the remote
// peer.
cleanUp = alice.restart(false, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 2)
// Restart the link + switch and check that the number of open circuits
// doesn't change.
cleanUp = alice.restart(true, false)
defer cleanUp()
alice.assertNumPendingNumOpenCircuits(2, 2)
}
// TestChannelLinkBandwidthChanReserve checks that the bandwidth available
// on the channel link reflects the channel reserve that must be kept
// at all times.
func TestChannelLinkBandwidthChanReserve(t *testing.T) {
t.Parallel()
// First start a link that has a balance greater than it's
// channel reserve.
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTimer, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
mockBlob [lnwire.OnionPacketSize]byte
coreLink = aliceLink.(*channelLink)
coreChan = coreLink.channel
defaultCommitFee = coreChan.StateSnapshot().CommitFee
aliceStartingBandwidth = aliceLink.Bandwidth()
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
estimator := chainfee.NewStaticEstimator(6000, 0)
feePerKw, err := estimator.EstimateFeePerKW(1)
if err != nil {
t.Fatalf("unable to query fee estimator: %v", err)
}
htlcFee := lnwire.NewMSatFromSatoshis(
feePerKw.FeeForWeight(input.HTLCWeight),
)
// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the channel
// reserve, commitment fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee-chanReserve) - htlcFee
assertLinkBandwidth(t, aliceLink, expectedBandwidth)
// Next, we'll create an HTLC worth 3 BTC, and send it into the link as
// a switch initiated payment. The resulting bandwidth should
// now be decremented to reflect the new HTLC.
htlcAmt := lnwire.NewMSatFromSatoshis(3 * btcutil.SatoshiPerBitcoin)
invoice, htlc, _, err := generatePayment(htlcAmt, htlcAmt, 5, mockBlob)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
addPkt := &htlcPacket{
htlc: htlc,
obfuscator: NewMockObfuscator(),
}
circuit := makePaymentCircuit(&htlc.PaymentHash, addPkt)
_, err = coreLink.cfg.Switch.commitCircuits(&circuit)
if err != nil {
t.Fatalf("unable to commit circuit: %v", err)
}
aliceLink.HandleSwitchPacket(addPkt)
time.Sleep(time.Millisecond * 100)
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// Alice should send the HTLC to Bob.
var msg lnwire.Message
select {
case msg = <-aliceMsgs:
case <-time.After(15 * time.Second):
t.Fatalf("did not receive message")
}
addHtlc, ok := msg.(*lnwire.UpdateAddHTLC)
if !ok {
t.Fatalf("expected UpdateAddHTLC, got %T", msg)
}
bobIndex, err := bobChannel.ReceiveHTLC(addHtlc)
if err != nil {
t.Fatalf("bob failed receiving htlc: %v", err)
}
// Lock in the HTLC.
if err := updateState(batchTimer, coreLink, bobChannel, true); err != nil {
t.Fatalf("unable to update state: %v", err)
}
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// If we now send in a valid HTLC settle for the prior HTLC we added,
// then the bandwidth should remain unchanged as the remote party will
// gain additional channel balance.
err = bobChannel.SettleHTLC(*invoice.Terms.PaymentPreimage, bobIndex, nil, nil, nil)
if err != nil {
t.Fatalf("unable to settle htlc: %v", err)
}
htlcSettle := &lnwire.UpdateFulfillHTLC{
ID: bobIndex,
PaymentPreimage: *invoice.Terms.PaymentPreimage,
}
aliceLink.HandleChannelUpdate(htlcSettle)
time.Sleep(time.Millisecond * 500)
// Since the settle is not locked in yet, Alice's bandwidth should still
// reflect that she has to pay the fee.
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt-htlcFee)
// Lock in the settle.
if err := updateState(batchTimer, coreLink, bobChannel, false); err != nil {
t.Fatalf("unable to update state: %v", err)
}
time.Sleep(time.Millisecond * 100)
assertLinkBandwidth(t, aliceLink, aliceStartingBandwidth-htlcAmt)
// Now we create a channel that has a channel reserve that is
// greater than it's balance. In these case only payments can
// be received on this channel, not sent. The available bandwidth
// should therefore be 0.
const bobChanAmt = btcutil.SatoshiPerBitcoin * 1
const bobChanReserve = btcutil.SatoshiPerBitcoin * 1.5
bobLink, _, _, start, bobCleanUp, _, err :=
newSingleLinkTestHarness(bobChanAmt, bobChanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer bobCleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
// Make sure bandwidth is reported as 0.
assertLinkBandwidth(t, bobLink, 0)
}
// TestChannelRetransmission tests the ability of the channel links to
// synchronize theirs states after abrupt disconnect.
func TestChannelRetransmission(t *testing.T) {
t.Parallel()
retransmissionTests := []struct {
name string
messages []expectedMessage
}{
{
// Tests the ability of the channel links states to be
// synchronized after remote node haven't receive
// revoke and ack message.
name: "intercept last alice revoke_and_ack",
messages: []expectedMessage{
// First initialization of the channel.
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
// Send payment from Alice to Bob and intercept
// the last revocation message, in this case
// Bob should not proceed the payment farther.
{"alice", "bob", &lnwire.UpdateAddHTLC{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, true},
// Reestablish messages exchange on nodes restart.
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
// Alice should resend the revoke_and_ack
// message to Bob because Bob claimed it in the
// re-establish message.
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
// Proceed the payment farther by sending the
// fulfilment message and trigger the state
// update.
{"bob", "alice", &lnwire.UpdateFulfillHTLC{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
},
},
{
// Tests the ability of the channel links states to be
// synchronized after remote node haven't receive
// revoke and ack message.
name: "intercept bob revoke_and_ack commit_sig messages",
messages: []expectedMessage{
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
// Send payment from Alice to Bob and intercept
// the last revocation message, in this case
// Bob should not proceed the payment farther.
{"alice", "bob", &lnwire.UpdateAddHTLC{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
// Intercept bob commit sig and revoke and ack
// messages.
{"bob", "alice", &lnwire.RevokeAndAck{}, true},
{"bob", "alice", &lnwire.CommitSig{}, true},
// Reestablish messages exchange on nodes restart.
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
// Bob should resend previously intercepted messages.
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
// Proceed the payment farther by sending the
// fulfilment message and trigger the state
// update.
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.UpdateFulfillHTLC{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
},
},
{
// Tests the ability of the channel links states to be
// synchronized after remote node haven't receive
// update and commit sig messages.
name: "intercept update add htlc and commit sig messages",
messages: []expectedMessage{
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
// Attempt make a payment from Alice to Bob,
// which is intercepted, emulating the Bob
// server abrupt stop.
{"alice", "bob", &lnwire.UpdateAddHTLC{}, true},
{"alice", "bob", &lnwire.CommitSig{}, true},
// Restart of the nodes, and after that nodes
// should exchange the reestablish messages.
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
// After Bob has notified Alice that he didn't
// receive updates Alice should re-send them.
{"alice", "bob", &lnwire.UpdateAddHTLC{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.UpdateFulfillHTLC{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
},
},
}
paymentWithRestart := func(t *testing.T, messages []expectedMessage) {
channels, cleanUp, restoreChannelsFromDb, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*5,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
chanID := lnwire.NewChanIDFromOutPoint(channels.aliceToBob.ChannelPoint())
serverErr := make(chan error, 4)
aliceInterceptor := createInterceptorFunc("[alice] <-- [bob]",
"alice", messages, chanID, false)
bobInterceptor := createInterceptorFunc("[alice] --> [bob]",
"bob", messages, chanID, false)
ct := newConcurrentTester(t)
// Add interceptor to check the order of Bob and Alice
// messages.
n := newThreeHopNetwork(ct,
channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob,
testStartingHeight,
)
n.aliceServer.intersect(aliceInterceptor)
n.bobServer.intersect(bobInterceptor)
if err := n.start(); err != nil {
ct.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
bobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink)
// Send payment which should fail because we intercept the
// update and commit messages.
//
// TODO(roasbeef); increase timeout?
receiver := n.bobServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, receiver, firstHop, hops, amount,
htlcAmt, totalTimelock,
).Wait(time.Second * 5)
if err == nil {
ct.Fatalf("payment shouldn't haven been finished")
}
// Stop network cluster and create new one, with the old
// channels states. Also do the *hack* - save the payment
// receiver to pass it in new channel link, otherwise payment
// will be failed because of the unknown payment hash. Hack
// will be removed with sphinx payment.
bobRegistry := n.bobServer.registry
n.stop()
channels, err = restoreChannelsFromDb()
if err != nil {
ct.Fatalf("unable to restore channels from database: %v", err)
}
n = newThreeHopNetwork(ct, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
n.firstBobChannelLink.cfg.Registry = bobRegistry
n.aliceServer.intersect(aliceInterceptor)
n.bobServer.intersect(bobInterceptor)
if err := n.start(); err != nil {
ct.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
// Wait for reestablishment to be proceeded and invoice to be settled.
// TODO(andrew.shvv) Will be removed if we move the notification center
// to the channel link itself.
var invoice channeldb.Invoice
for i := 0; i < 20; i++ {
select {
case <-time.After(time.Millisecond * 200):
case serverErr := <-serverErr:
ct.Fatalf("server error: %v", serverErr)
}
// Check that alice invoice wasn't settled and
// bandwidth of htlc links hasn't been changed.
invoice, err = receiver.registry.LookupInvoice(rhash)
if err != nil {
err = errors.Errorf("unable to get invoice: %v", err)
continue
}
if invoice.State != channeldb.ContractSettled {
err = errors.Errorf("alice invoice haven't been settled")
continue
}
aliceExpectedBandwidth := aliceBandwidthBefore - htlcAmt
if aliceExpectedBandwidth != n.aliceChannelLink.Bandwidth() {
err = errors.Errorf("expected alice to have %v, instead has %v",
aliceExpectedBandwidth, n.aliceChannelLink.Bandwidth())
continue
}
bobExpectedBandwidth := bobBandwidthBefore + htlcAmt
if bobExpectedBandwidth != n.firstBobChannelLink.Bandwidth() {
err = errors.Errorf("expected bob to have %v, instead has %v",
bobExpectedBandwidth, n.firstBobChannelLink.Bandwidth())
continue
}
break
}
if err != nil {
ct.Fatal(err)
}
}
for _, test := range retransmissionTests {
passed := t.Run(test.name, func(t *testing.T) {
paymentWithRestart(t, test.messages)
})
if !passed {
break
}
}
}
// TestShouldAdjustCommitFee tests the shouldAdjustCommitFee pivot function to
// ensure that ie behaves properly. We should only update the fee if it
// deviates from our current fee by more 10% or more.
func TestShouldAdjustCommitFee(t *testing.T) {
tests := []struct {
netFee chainfee.SatPerKWeight
chanFee chainfee.SatPerKWeight
shouldAdjust bool
}{
// The network fee is 3x lower than the current commitment
// transaction. As a result, we should adjust our fee to match
// it.
{
netFee: 100,
chanFee: 3000,
shouldAdjust: true,
},
// The network fee is lower than the current commitment fee,
// but only slightly so, so we won't update the commitment fee.
{
netFee: 2999,
chanFee: 3000,
shouldAdjust: false,
},
// The network fee is lower than the commitment fee, but only
// right before it crosses our current threshold.
{
netFee: 1000,
chanFee: 1099,
shouldAdjust: false,
},
// The network fee is lower than the commitment fee, and within
// our range of adjustment, so we should adjust.
{
netFee: 1000,
chanFee: 1100,
shouldAdjust: true,
},
// The network fee is 2x higher than our commitment fee, so we
// should adjust upwards.
{
netFee: 2000,
chanFee: 1000,
shouldAdjust: true,
},
// The network fee is higher than our commitment fee, but only
// slightly so, so we won't update.
{
netFee: 1001,
chanFee: 1000,
shouldAdjust: false,
},
// The network fee is higher than our commitment fee, but
// hasn't yet crossed our activation threshold.
{
netFee: 1100,
chanFee: 1099,
shouldAdjust: false,
},
// The network fee is higher than our commitment fee, and
// within our activation threshold, so we should update our
// fee.
{
netFee: 1100,
chanFee: 1000,
shouldAdjust: true,
},
// Our fees match exactly, so we shouldn't update it at all.
{
netFee: 1000,
chanFee: 1000,
shouldAdjust: false,
},
}
for i, test := range tests {
adjustedFee := shouldAdjustCommitFee(
test.netFee, test.chanFee,
)
if adjustedFee && !test.shouldAdjust {
t.Fatalf("test #%v failed: net_fee=%v, "+
"chan_fee=%v, adjust_expect=%v, adjust_returned=%v",
i, test.netFee, test.chanFee, test.shouldAdjust,
adjustedFee)
}
}
}
// TestChannelLinkShutdownDuringForward asserts that a link can be fully
// stopped when it is trying to send synchronously through the switch. The
// specific case this can occur is when a link forwards incoming Adds. We test
// this by forcing the switch into a state where it will not accept new packets,
// and then killing the link, which can only succeed if forwarding can be
// canceled by a call to Stop.
func TestChannelLinkShutdownDuringForward(t *testing.T) {
t.Parallel()
// First, we'll create our traditional three hop network. We're
// interested in testing the ability to stop the link when it is
// synchronously forwarding to the switch, which happens when an
// incoming link forwards Adds. Thus, the test will be performed
// against Bob's first link.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
defer n.feeEstimator.Stop()
// Define a helper method that strobes the switch's log ticker, and
// unblocks after nothing has been pulled for two seconds.
waitForBobsSwitchToBlock := func() {
bobSwitch := n.firstBobChannelLink.cfg.Switch
ticker := bobSwitch.cfg.LogEventTicker.(*ticker.Force)
timeout := time.After(15 * time.Second)
for {
time.Sleep(50 * time.Millisecond)
select {
case ticker.Force <- time.Now():
case <-time.After(2 * time.Second):
return
case <-timeout:
t.Fatalf("switch did not block")
}
}
}
// Define a helper method that strobes the link's batch ticker, and
// unblocks after nothing has been pulled for two seconds.
waitForBobsIncomingLinkToBlock := func() {
ticker := n.firstBobChannelLink.cfg.BatchTicker.(*ticker.Force)
timeout := time.After(15 * time.Second)
for {
time.Sleep(50 * time.Millisecond)
select {
case ticker.Force <- time.Now():
case <-time.After(2 * time.Second):
// We'll give a little extra time here, to
// ensure that the packet is being pressed
// against the htlcPlex.
time.Sleep(50 * time.Millisecond)
return
case <-timeout:
t.Fatalf("link did not block")
}
}
}
// To test that the cancellation is happening properly, we will set the
// switch's htlcPlex to nil, so that calls to routeAsync block, and can
// only exit if the link (or switch) is exiting. We will only be testing
// the link here.
//
// In order to avoid data races, we need to ensure the switch isn't
// selecting on that channel in the meantime. We'll prevent this by
// first acquiring the index mutex and forcing a log event so that the
// htlcForwarder is blocked inside the logTicker case, which also needs
// the indexMtx.
n.firstBobChannelLink.cfg.Switch.indexMtx.Lock()
// Strobe the log ticker, and wait for switch to stop accepting any more
// log ticks.
waitForBobsSwitchToBlock()
// While the htlcForwarder is blocked, swap out the htlcPlex with a nil
// channel, and unlock the indexMtx to allow return to the
// htlcForwarder's main select. After this, any attempt to forward
// through the switch will block.
n.firstBobChannelLink.cfg.Switch.htlcPlex = nil
n.firstBobChannelLink.cfg.Switch.indexMtx.Unlock()
// Now, make a payment from Alice to Carol, which should cause Bob's
// incoming link to block when it tries to submit the packet to the nil
// htlcPlex.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(
amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink,
)
firstHop := n.firstBobChannelLink.ShortChanID()
makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
)
// Strobe the batch ticker of Bob's incoming link, waiting for it to
// become fully blocked.
waitForBobsIncomingLinkToBlock()
// Finally, stop the link to test that it can exit while synchronously
// forwarding Adds to the switch.
done := make(chan struct{})
go func() {
n.firstBobChannelLink.Stop()
close(done)
}()
select {
case <-time.After(3 * time.Second):
t.Fatalf("unable to shutdown link while fwding incoming Adds")
case <-done:
}
}
// TestChannelLinkUpdateCommitFee tests that when a new block comes in, the
// channel link properly checks to see if it should update the commitment fee.
func TestChannelLinkUpdateCommitFee(t *testing.T) {
t.Parallel()
// First, we'll create our traditional three hop network. We'll only be
// interacting with and asserting the state of two of the end points
// for this test.
const aliceInitialBalance = btcutil.SatoshiPerBitcoin * 3
channels, cleanUp, _, err := createClusterChannels(
aliceInitialBalance, btcutil.SatoshiPerBitcoin*5,
)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
// First, we'll set up some message interceptors to ensure that the
// proper messages are sent when updating fees.
chanID := n.aliceChannelLink.ChanID()
messages := []expectedMessage{
{"alice", "bob", &lnwire.ChannelReestablish{}, false},
{"bob", "alice", &lnwire.ChannelReestablish{}, false},
{"alice", "bob", &lnwire.FundingLocked{}, false},
{"bob", "alice", &lnwire.FundingLocked{}, false},
// First fee update.
{"alice", "bob", &lnwire.UpdateFee{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
// Second fee update.
{"alice", "bob", &lnwire.UpdateFee{}, false},
{"alice", "bob", &lnwire.CommitSig{}, false},
{"bob", "alice", &lnwire.RevokeAndAck{}, false},
{"bob", "alice", &lnwire.CommitSig{}, false},
{"alice", "bob", &lnwire.RevokeAndAck{}, false},
}
n.aliceServer.intersect(createInterceptorFunc("[alice] <-- [bob]",
"alice", messages, chanID, false))
n.bobServer.intersect(createInterceptorFunc("[alice] --> [bob]",
"bob", messages, chanID, false))
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
defer n.feeEstimator.Stop()
startingFeeRate := channels.aliceToBob.CommitFeeRate()
// triggerFeeUpdate is a helper closure to determine whether a fee
// update was triggered and completed properly.
triggerFeeUpdate := func(feeEstimate, newFeeRate chainfee.SatPerKWeight,
shouldUpdate bool) {
t.Helper()
// Record the fee rates before the links process the fee update
// to test the case where a fee update isn't triggered.
aliceBefore := channels.aliceToBob.CommitFeeRate()
bobBefore := channels.bobToAlice.CommitFeeRate()
// For the sake of this test, we'll reset the timer so that
// Alice's link queries for a new network fee.
n.aliceChannelLink.updateFeeTimer.Reset(time.Millisecond)
// Next, we'll send the first fee rate response to Alice.
select {
case n.feeEstimator.byteFeeIn <- feeEstimate:
case <-time.After(time.Second * 5):
t.Fatalf("alice didn't query for the new network fee")
}
// Record the fee rates after the links have processed the fee
// update and ensure they are correct based on whether a fee
// update should have been triggered.
require.Eventually(t, func() bool {
aliceAfter := channels.aliceToBob.CommitFeeRate()
bobAfter := channels.bobToAlice.CommitFeeRate()
switch {
case shouldUpdate && aliceAfter != newFeeRate:
return false
case shouldUpdate && bobAfter != newFeeRate:
return false
case !shouldUpdate && aliceAfter != aliceBefore:
return false
case !shouldUpdate && bobAfter != bobBefore:
return false
}
return true
}, 10*time.Second, time.Second)
}
// Triggering the link to update the fee of the channel with the same
// fee rate should not send a fee update.
triggerFeeUpdate(startingFeeRate, startingFeeRate, false)
// Triggering the link to update the fee of the channel with a much
// larger fee rate _should_ send a fee update.
newFeeRate := startingFeeRate * 3
triggerFeeUpdate(newFeeRate, newFeeRate, true)
// Triggering the link to update the fee of the channel with a fee rate
// that exceeds its maximum fee allocation should result in a fee rate
// corresponding to the maximum fee allocation.
const maxFeeRate chainfee.SatPerKWeight = 207182320
triggerFeeUpdate(maxFeeRate+1, maxFeeRate, true)
}
// TestChannelLinkAcceptDuplicatePayment tests that if a link receives an
// incoming HTLC for a payment we have already settled, then it accepts the
// HTLC. We do this to simplify the processing of settles after restarts or
// failures, reducing ambiguity when a batch is only partially processed.
func TestChannelLinkAcceptDuplicatePayment(t *testing.T) {
t.Parallel()
// First, we'll create our traditional three hop network. We'll only be
// interacting with and asserting the state of two of the end points
// for this test.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
// We'll start off by making a payment from Alice to Carol. We'll
// manually generate this request so we can control all the parameters.
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink)
blob, err := generateRoute(hops...)
if err != nil {
t.Fatal(err)
}
invoice, htlc, pid, err := generatePayment(
amount, htlcAmt, totalTimelock, blob,
)
if err != nil {
t.Fatal(err)
}
err = n.carolServer.registry.AddInvoice(*invoice, htlc.PaymentHash)
if err != nil {
t.Fatalf("unable to add invoice in carol registry: %v", err)
}
// With the invoice now added to Carol's registry, we'll send the
// payment.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
if err != nil {
t.Fatalf("unable to send payment to carol: %v", err)
}
resultChan, err := n.aliceServer.htlcSwitch.GetPaymentResult(
pid, htlc.PaymentHash, newMockDeobfuscator(),
)
if err != nil {
t.Fatalf("unable to get payment result: %v", err)
}
// Now, if we attempt to send the payment *again* it should be rejected
// as it's a duplicate request.
err = n.aliceServer.htlcSwitch.SendHTLC(
n.firstBobChannelLink.ShortChanID(), pid, htlc,
)
if err != ErrDuplicateAdd {
t.Fatalf("ErrDuplicateAdd should have been "+
"received got: %v", err)
}
select {
case result, ok := <-resultChan:
if !ok {
t.Fatalf("unexpected shutdown")
}
if result.Error != nil {
t.Fatalf("payment failed: %v", result.Error)
}
case <-time.After(5 * time.Second):
t.Fatalf("payment result did not arrive")
}
}
// TestChannelLinkAcceptOverpay tests that if we create an invoice for sender,
// and the sender sends *more* than specified in the invoice, then we'll still
// accept it and settle as normal.
func TestChannelLinkAcceptOverpay(t *testing.T) {
t.Parallel()
// First, we'll create our traditional three hop network. We'll only be
// interacting with and asserting the state of two of the end points
// for this test.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
// We'll request a route to send 10k satoshis via Alice -> Bob ->
// Carol.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(
amount, testStartingHeight,
n.firstBobChannelLink, n.carolChannelLink,
)
// When we actually go to send the payment, we'll actually create an
// invoice at Carol for only half of this amount.
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount/2, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
// Wait for Alice and Bob's second link to receive the revocation.
time.Sleep(2 * time.Second)
// Even though we sent 2x what was asked for, Carol should still have
// accepted the payment and marked it as settled.
invoice, err := receiver.registry.LookupInvoice(rhash)
if err != nil {
t.Fatalf("unable to get invoice: %v", err)
}
if invoice.State != channeldb.ContractSettled {
t.Fatal("carol invoice haven't been settled")
}
expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
if expectedAliceBandwidth != n.aliceChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth())
}
expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
if expectedBobBandwidth1 != n.firstBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth())
}
expectedBobBandwidth2 := secondBobBandwidthBefore - amount
if expectedBobBandwidth2 != n.secondBobChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth())
}
expectedCarolBandwidth := carolBandwidthBefore + amount
if expectedCarolBandwidth != n.carolChannelLink.Bandwidth() {
t.Fatalf("channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth())
}
// Finally, we'll ensure that the amount we paid is properly reflected
// in the stored invoice.
if invoice.AmtPaid != amount {
t.Fatalf("expected amt paid to be %v, is instead %v", amount,
invoice.AmtPaid)
}
}
// persistentLinkHarness is used to control the lifecylce of a link and the
// switch that operates it. It supports the ability to restart either the link
// or both the link and the switch.
type persistentLinkHarness struct {
t *testing.T
link ChannelLink
coreLink *channelLink
channel *lnwallet.LightningChannel
batchTicker chan time.Time
msgs chan lnwire.Message
restoreChan func() (*lnwallet.LightningChannel, error)
}
// newPersistentLinkHarness initializes a new persistentLinkHarness and derives
// the supporting references from the active link.
func newPersistentLinkHarness(t *testing.T, link ChannelLink,
batchTicker chan time.Time,
restore func() (*lnwallet.LightningChannel,
error)) *persistentLinkHarness {
coreLink := link.(*channelLink)
return &persistentLinkHarness{
t: t,
link: link,
coreLink: coreLink,
channel: coreLink.channel,
batchTicker: batchTicker,
msgs: coreLink.cfg.Peer.(*mockPeer).sentMsgs,
restoreChan: restore,
}
}
// restart facilitates a shutdown and restart of the link maintained by the
// harness. The primary purpose of this method is to ensure the consistency of
// the supporting references is maintained across restarts.
//
// If `restartSwitch` is set, the entire switch will also be restarted,
// and will be reinitialized with the contents of the channeldb backing Alice's
// channel.
//
// Any number of hodl flags can be passed as additional arguments to this
// method. If none are provided, the mask will be extracted as hodl.MaskNone.
func (h *persistentLinkHarness) restart(restartSwitch, syncStates bool,
hodlFlags ...hodl.Flag) func() {
// First, remove the link from the switch.
h.coreLink.cfg.Switch.RemoveLink(h.link.ChanID())
if restartSwitch {
// If a switch restart is requested, we will stop it. It will be
// reinstantiated in restartLink.
h.coreLink.cfg.Switch.Stop()
}
// Since our in-memory state may have diverged from our persistent
// state, we will restore the persisted state to ensure we always start
// the link in a consistent state.
var err error
h.channel, err = h.restoreChan()
if err != nil {
h.t.Fatalf("unable to restore channels: %v", err)
}
// Now, restart the link using the channel state. This will take care of
// adding the link to an existing switch, or creating a new one using
// the database owned by the link.
var cleanUp func()
h.link, h.batchTicker, cleanUp, err = h.restartLink(
h.channel, restartSwitch, syncStates, hodlFlags,
)
if err != nil {
h.t.Fatalf("unable to restart alicelink: %v", err)
}
// Repopulate the remaining fields in the harness.
h.coreLink = h.link.(*channelLink)
h.msgs = h.coreLink.cfg.Peer.(*mockPeer).sentMsgs
return cleanUp
}
// checkSent reads the links message stream and verify that the messages are
// dequeued in the same order as provided by `pkts`.
func (h *persistentLinkHarness) checkSent(pkts []*htlcPacket) {
for _, pkt := range pkts {
var msg lnwire.Message
select {
case msg = <-h.msgs:
case <-time.After(15 * time.Second):
h.t.Fatalf("did not receive message")
}
if !reflect.DeepEqual(msg, pkt.htlc) {
h.t.Fatalf("unexpected packet, want %v, got %v",
pkt.htlc, msg)
}
}
}
// commitCircuits accepts a list of circuits and tries to commit them to the
// switch's circuit map. The forwarding actions are returned if there was no
// failure.
func (h *persistentLinkHarness) commitCircuits(circuits []*PaymentCircuit) *CircuitFwdActions {
fwdActions, err := h.coreLink.cfg.Switch.commitCircuits(circuits...)
if err != nil {
h.t.Fatalf("unable to commit circuit: %v", err)
}
return fwdActions
}
func (h *persistentLinkHarness) assertNumPendingNumOpenCircuits(
wantPending, wantOpen int) {
_, _, line, _ := runtime.Caller(1)
numPending := h.coreLink.cfg.Switch.circuits.NumPending()
if numPending != wantPending {
h.t.Fatalf("line: %d: wrong number of pending circuits: "+
"want %d, got %d", line, wantPending, numPending)
}
numOpen := h.coreLink.cfg.Switch.circuits.NumOpen()
if numOpen != wantOpen {
h.t.Fatalf("line: %d: wrong number of open circuits: "+
"want %d, got %d", line, wantOpen, numOpen)
}
}
// trySignNextCommitment signals the batch ticker so that the link will try to
// update its commitment transaction.
func (h *persistentLinkHarness) trySignNextCommitment() {
select {
case h.batchTicker <- time.Now():
// Give the link enough time to process the request.
time.Sleep(time.Millisecond * 500)
case <-time.After(15 * time.Second):
h.t.Fatalf("did not initiate state transition")
}
}
// restartLink creates a new channel link from the given channel state, and adds
// to an htlcswitch. If none is provided by the caller, a new one will be
// created using Alice's database.
func (h *persistentLinkHarness) restartLink(
aliceChannel *lnwallet.LightningChannel, restartSwitch, syncStates bool,
hodlFlags []hodl.Flag) (
ChannelLink, chan time.Time, func(), error) {
var (
decoder = newMockIteratorDecoder()
obfuscator = NewMockObfuscator()
alicePeer = &mockPeer{
sentMsgs: make(chan lnwire.Message, 2000),
quit: make(chan struct{}),
}
globalPolicy = ForwardingPolicy{
MinHTLCOut: lnwire.NewMSatFromSatoshis(5),
BaseFee: lnwire.NewMSatFromSatoshis(1),
TimeLockDelta: 6,
}
pCache = newMockPreimageCache()
)
aliceDb := aliceChannel.State().Db
aliceSwitch := h.coreLink.cfg.Switch
if restartSwitch {
var err error
aliceSwitch, err = initSwitchWithDB(testStartingHeight, aliceDb)
if err != nil {
return nil, nil, nil, err
}
}
// Instantiate with a long interval, so that we can precisely control
// the firing via force feeding.
bticker := ticker.NewForce(time.Hour)
aliceCfg := ChannelLinkConfig{
FwrdingPolicy: globalPolicy,
Peer: alicePeer,
Switch: aliceSwitch,
Circuits: aliceSwitch.CircuitModifier(),
ForwardPackets: aliceSwitch.ForwardPackets,
DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) (
hop.ErrorEncrypter, lnwire.FailCode) {
return obfuscator, lnwire.CodeNone
},
FetchLastChannelUpdate: mockGetChanUpdateMessage,
PreimageCache: pCache,
OnChannelFailure: func(lnwire.ChannelID,
lnwire.ShortChannelID, LinkFailureError) {
},
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
return nil
},
Registry: h.coreLink.cfg.Registry,
FeeEstimator: newMockFeeEstimator(),
ChainEvents: &contractcourt.ChainEventSubscription{},
BatchTicker: bticker,
FwdPkgGCTicker: ticker.New(5 * time.Second),
PendingCommitTicker: ticker.New(time.Minute),
// Make the BatchSize and Min/MaxFeeUpdateTimeout large enough
// to not trigger commit updates automatically during tests.
BatchSize: 10000,
MinFeeUpdateTimeout: 30 * time.Minute,
MaxFeeUpdateTimeout: 40 * time.Minute,
// Set any hodl flags requested for the new link.
HodlMask: hodl.MaskFromFlags(hodlFlags...),
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
MaxFeeAllocation: DefaultMaxLinkFeeAllocation,
NotifyActiveLink: func(wire.OutPoint) {},
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
SyncStates: syncStates,
}
aliceLink := NewChannelLink(aliceCfg, aliceChannel)
if err := aliceSwitch.AddLink(aliceLink); err != nil {
return nil, nil, nil, err
}
go func() {
for {
select {
case <-aliceLink.(*channelLink).htlcUpdates:
case <-aliceLink.(*channelLink).quit:
return
}
}
}()
cleanUp := func() {
close(alicePeer.quit)
defer aliceLink.Stop()
}
return aliceLink, bticker.Force, cleanUp, nil
}
// gnerateHtlc generates a simple payment from Bob to Alice.
func generateHtlc(t *testing.T, coreLink *channelLink,
id uint64) *lnwire.UpdateAddHTLC {
t.Helper()
htlc, invoice := generateHtlcAndInvoice(t, id)
// We must add the invoice to the registry, such that Alice
// expects this payment.
err := coreLink.cfg.Registry.(*mockInvoiceRegistry).AddInvoice(
*invoice, htlc.PaymentHash,
)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
return htlc
}
// generateHtlcAndInvoice generates an invoice and a single hop htlc to send to
// the receiver.
func generateHtlcAndInvoice(t *testing.T,
id uint64) (*lnwire.UpdateAddHTLC, *channeldb.Invoice) {
t.Helper()
htlcAmt := lnwire.NewMSatFromSatoshis(10000)
htlcExpiry := testStartingHeight + testInvoiceCltvExpiry
hops := []*hop.Payload{
hop.NewLegacyPayload(&sphinx.HopData{
Realm: [1]byte{}, // hop.BitcoinNetwork
NextAddress: [8]byte{}, // hop.Exit,
ForwardAmount: uint64(htlcAmt),
OutgoingCltv: uint32(htlcExpiry),
}),
}
blob, err := generateRoute(hops...)
if err != nil {
t.Fatalf("unable to generate route: %v", err)
}
invoice, htlc, _, err := generatePayment(
htlcAmt, htlcAmt, uint32(htlcExpiry), blob,
)
if err != nil {
t.Fatalf("unable to create payment: %v", err)
}
htlc.ID = id
return htlc, invoice
}
// TestChannelLinkNoMoreUpdates tests that we won't send a new commitment
// when there are no new updates to sign.
func TestChannelLinkNoMoreUpdates(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, _, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
// Add two HTLCs to Alice's registry, that Bob can pay.
htlc1 := generateHtlc(t, coreLink, 0)
htlc2 := generateHtlc(t, coreLink, 1)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
// We now play out the following scanario:
//
// (1) Alice receives htlc1 from Bob.
// (2) Bob sends signature covering htlc1.
// (3) Alice receives htlc2 from Bob.
// (4) Since Bob has sent a new commitment signature, Alice should
// first respond with a revocation.
// (5) Alice should also send a commitment signature for the new state,
// covering htlc1.
// (6) Bob sends a new commitment signature, covering htlc2 that he sent
// earlier. This signature should cover hltc1 + htlc2.
// (7) Alice should revoke the old commitment. This ACKs htlc2.
// (8) Bob can now revoke his old commitment in response to the
// signature Alice sent covering htlc1.
// (9) htlc1 is now locked in on Bob's commitment, and we expect Alice
// to settle it.
// (10) Alice should send a signature covering this settle to Bob. Only
// htlc2 should now be covered by this signature.
// (11) Bob can revoke his last state, which will also ACK the settle
// of htlc1.
// (12) Bob sends a new commitment signature. This signature should
// cover htlc2.
// (13) Alice will send a settle for htlc2.
// (14) Alice will also send a signature covering the settle.
// (15) Alice should send a revocation in response to the signature Bob
// sent earlier.
// (16) Bob will revoke his commitment in response to the commitment
// Alice sent.
// (17) Send a signature for the empty state. No HTLCs are left.
// (18) Alice will revoke her previous state.
// Alice Bob
// | |
// | ... |
// | | <--- idle (no htlc on either side)
// | |
ctx.sendHtlcBobToAlice(htlc1) // |<----- add-1 ------| (1)
ctx.sendCommitSigBobToAlice(1) // |<------ sig -------| (2)
ctx.sendHtlcBobToAlice(htlc2) // |<----- add-2 ------| (3)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (4) <--- Alice acks add-1
ctx.receiveCommitSigAliceToBob(1) // |------- sig ------>| (5) <--- Alice signs add-1
ctx.sendCommitSigBobToAlice(2) // |<------ sig -------| (6)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (7) <--- Alice acks add-2
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (8)
ctx.receiveSettleAliceToBob() // |------ ful-1 ----->| (9)
ctx.receiveCommitSigAliceToBob(1) // |------- sig ------>| (10) <--- Alice signs add-1 + add-2 + ful-1 = add-2
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (11)
ctx.sendCommitSigBobToAlice(1) // |<------ sig -------| (12)
ctx.receiveSettleAliceToBob() // |------ ful-2 ----->| (13)
ctx.receiveCommitSigAliceToBob(0) // |------- sig ------>| (14) <--- Alice signs add-2 + ful-2 = no htlcs
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (15)
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (16) <--- Bob acks that there are no more htlcs
ctx.sendCommitSigBobToAlice(0) // |<------ sig -------| (17)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (18) <--- Alice acks that there are no htlcs on Alice's side
// No there are no more changes to ACK or sign, make sure Alice doesn't
// attempt to send any more messages.
var msg lnwire.Message
select {
case msg = <-aliceMsgs:
t.Fatalf("did not expect message %T", msg)
case <-time.After(100 * time.Millisecond):
}
}
// checkHasPreimages inspects Alice's preimage cache, and asserts whether the
// preimages for the provided HTLCs are known and unknown, and that all of them
// match the expected status of expOk.
func checkHasPreimages(t *testing.T, coreLink *channelLink,
htlcs []*lnwire.UpdateAddHTLC, expOk bool) {
t.Helper()
err := wait.NoError(func() error {
for i := range htlcs {
_, ok := coreLink.cfg.PreimageCache.LookupPreimage(
htlcs[i].PaymentHash,
)
if ok == expOk {
continue
}
return fmt.Errorf("expected to find witness: %v, "+
"got %v for hash=%x", expOk, ok,
htlcs[i].PaymentHash)
}
return nil
}, 5*time.Second)
if err != nil {
t.Fatalf("unable to find preimages: %v", err)
}
}
// TestChannelLinkWaitForRevocation tests that we will keep accepting updates
// to our commitment transaction, even when we are waiting for a revocation
// from the remote node.
func TestChannelLinkWaitForRevocation(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, _, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
// We will send 10 HTLCs in total, from Bob to Alice.
numHtlcs := 10
var htlcs []*lnwire.UpdateAddHTLC
for i := 0; i < numHtlcs; i++ {
htlc := generateHtlc(t, coreLink, uint64(i))
htlcs = append(htlcs, htlc)
}
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
assertNoMsgFromAlice := func() {
select {
case <-aliceMsgs:
t.Fatalf("did not expect message from Alice")
case <-time.After(50 * time.Millisecond):
}
}
// We play out the following scenario:
//
// (1) Add the first HTLC.
// (2) Bob sends signature covering the htlc.
// (3) Since Bob has sent a new commitment signature, Alice should first
// respond with a revocation. This revocation will ACK the first htlc.
// (4) Alice should also send a commitment signature for the new state,
// locking in the HTLC on Bob's commitment. Note that we don't
// immediately let Bob respond with a revocation in this case.
// (5.i) Now we send the rest of the HTLCs from Bob to Alice.
// (6.i) Bob sends a new commitment signature, covering all HTLCs up
// to this point.
// (7.i) Alice should respond to Bob's state updates with revocations,
// but cannot send any new signatures for Bob's state because her
// revocation window is exhausted.
// (8) Now let Bob finally send his revocation.
// (9) We expect Alice to settle her first HTLC, since it was already
// locked in.
// (10) Now Alice should send a signature covering this settle + lock
// in the rest of the HTLCs on Bob's commitment.
// (11) Bob receives the new signature for his commitment, and can
// revoke his old state, ACKing the settle.
// (12.i) Now Alice can settle all the HTLCs, since they are locked in
// on both parties' commitments.
// (13) Bob can send a signature covering the first settle Alice sent.
// Bob's signature should cover all the remaining HTLCs as well, since
// he hasn't ACKed the last settles yet. Alice receives the signature
// from Bob. Alice's commitment now has the first HTLC settled, and all
// the other HTLCs locked in.
// (14) Alice will send a signature for all the settles she just sent.
// (15) Bob can revoke his previous state, in response to Alice's
// signature.
// (16) In response to the signature Bob sent, Alice can
// revoke her previous state.
// (17) Bob still hasn't sent a commitment covering all settles, so do
// that now. Since Bob ACKed all settles, no HTLCs should be left on
// the commitment.
// (18) Alice will revoke her previous state.
// Alice Bob
// | |
// | ... |
// | | <--- idle (no htlc on either side)
// | |
ctx.sendHtlcBobToAlice(htlcs[0]) // |<----- add-1 ------| (1)
ctx.sendCommitSigBobToAlice(1) // |<------ sig -------| (2)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (3) <--- Alice acks add-1
ctx.receiveCommitSigAliceToBob(1) // |------- sig ------>| (4) <--- Alice signs add-1
for i := 1; i < numHtlcs; i++ { // | |
ctx.sendHtlcBobToAlice(htlcs[i]) // |<----- add-i ------| (5.i)
ctx.sendCommitSigBobToAlice(i + 1) // |<------ sig -------| (6.i)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (7.i) <--- Alice acks add-i
assertNoMsgFromAlice() // | |
// | | Alice should not send a sig for
// | | Bob's last state, since she is
// | | still waiting for a revocation
// | | for the previous one.
} // | |
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (8) Finally let Bob send rev
ctx.receiveSettleAliceToBob() // |------ ful-1 ----->| (9)
ctx.receiveCommitSigAliceToBob(numHtlcs - 1) // |------- sig ------>| (10) <--- Alice signs add-i
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (11)
for i := 1; i < numHtlcs; i++ { // | |
ctx.receiveSettleAliceToBob() // |------ ful-1 ----->| (12.i)
} // | |
ctx.sendCommitSigBobToAlice(numHtlcs - 1) // |<------ sig -------| (13)
ctx.receiveCommitSigAliceToBob(0) // |------- sig ------>| (14)
ctx.sendRevAndAckBobToAlice() // |<------ rev -------| (15)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (16)
ctx.sendCommitSigBobToAlice(0) // |<------ sig -------| (17)
ctx.receiveRevAndAckAliceToBob() // |------- rev ------>| (18)
// Both side's state is now updated, no more messages should be sent.
assertNoMsgFromAlice()
}
// TestChannelLinkNoEmptySig asserts that no empty commit sig message is sent
// when the commitment txes are out of sync.
func TestChannelLinkNoEmptySig(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
defer aliceLink.Stop()
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
// Send htlc 1 from Alice to Bob.
htlc1, _ := generateHtlcAndInvoice(t, 0)
ctx.sendHtlcAliceToBob(0, htlc1)
ctx.receiveHtlcAliceToBob()
// Tick the batch ticker to trigger a commitsig from Alice->Bob.
select {
case batchTicker <- time.Now():
case <-time.After(5 * time.Second):
t.Fatalf("could not force commit sig")
}
// Receive a CommitSig from Alice covering the Add from above.
ctx.receiveCommitSigAliceToBob(1)
// Bob revokes previous commitment tx.
ctx.sendRevAndAckBobToAlice()
// Alice sends htlc 2 to Bob.
htlc2, _ := generateHtlcAndInvoice(t, 0)
ctx.sendHtlcAliceToBob(1, htlc2)
ctx.receiveHtlcAliceToBob()
// Tick the batch ticker to trigger a commitsig from Alice->Bob.
select {
case batchTicker <- time.Now():
case <-time.After(5 * time.Second):
t.Fatalf("could not force commit sig")
}
// Get the commit sig from Alice, but don't send it to Bob yet.
commitSigAlice := ctx.receiveCommitSigAlice(2)
// Bob adds htlc 1 to its remote commit tx.
ctx.sendCommitSigBobToAlice(1)
// Now send Bob the signature from Alice covering both htlcs.
err = bobChannel.ReceiveNewCommitment(
commitSigAlice.CommitSig, commitSigAlice.HtlcSigs,
)
if err != nil {
t.Fatalf("bob failed receiving commitment: %v", err)
}
// Both Alice and Bob revoke their previous commitment txes.
ctx.receiveRevAndAckAliceToBob()
ctx.sendRevAndAckBobToAlice()
// The commit txes are not in sync, but it is Bob's turn to send a new
// signature. We don't expect Alice to send out any message. This check
// allows some time for the log commit ticker to trigger for Alice.
ctx.assertNoMsgFromAlice(time.Second)
}
// TestChannelLinkBatchPreimageWrite asserts that a link will batch preimage
// writes when just as it receives a CommitSig to lock in any Settles, and also
// if the link is aware of any uncommitted preimages if the link is stopped,
// i.e. due to a disconnection or shutdown.
func TestChannelLinkBatchPreimageWrite(t *testing.T) {
t.Parallel()
tests := []struct {
name string
disconnect bool
}{
{
name: "flush on commit sig",
disconnect: false,
},
{
name: "flush on disconnect",
disconnect: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
testChannelLinkBatchPreimageWrite(t, test.disconnect)
})
}
}
func testChannelLinkBatchPreimageWrite(t *testing.T, disconnect bool) {
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, startUp, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := startUp(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
// We will send 10 HTLCs in total, from Bob to Alice.
numHtlcs := 10
var htlcs []*lnwire.UpdateAddHTLC
var invoices []*channeldb.Invoice
for i := 0; i < numHtlcs; i++ {
htlc, invoice := generateHtlcAndInvoice(t, uint64(i))
htlcs = append(htlcs, htlc)
invoices = append(invoices, invoice)
}
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
// First, send a batch of Adds from Alice to Bob.
for i, htlc := range htlcs {
ctx.sendHtlcAliceToBob(i, htlc)
ctx.receiveHtlcAliceToBob()
}
// Assert that no preimages exist for these htlcs in Alice's cache.
checkHasPreimages(t, coreLink, htlcs, false)
// Force alice's link to sign a commitment covering the htlcs sent thus
// far.
select {
case batchTicker <- time.Now():
case <-time.After(15 * time.Second):
t.Fatalf("could not force commit sig")
}
// Do a commitment dance to lock in the Adds, we expect numHtlcs htlcs
// to be on each party's commitment transactions.
ctx.receiveCommitSigAliceToBob(numHtlcs)
ctx.sendRevAndAckBobToAlice()
ctx.sendCommitSigBobToAlice(numHtlcs)
ctx.receiveRevAndAckAliceToBob()
// Check again that no preimages exist for these htlcs in Alice's cache.
checkHasPreimages(t, coreLink, htlcs, false)
// Now, have Bob settle the HTLCs back to Alice using the preimages in
// the invoice corresponding to each of the HTLCs.
for i, invoice := range invoices {
ctx.sendSettleBobToAlice(
uint64(i),
*invoice.Terms.PaymentPreimage,
)
}
// Assert that Alice has not yet written the preimages, even though she
// has received them in the UpdateFulfillHTLC messages.
checkHasPreimages(t, coreLink, htlcs, false)
// If this is the disconnect run, we will having Bob send Alice his
// CommitSig, and simply stop Alice's link. As she exits, we should
// detect that she has uncommitted preimages and write them to disk.
if disconnect {
aliceLink.Stop()
checkHasPreimages(t, coreLink, htlcs, true)
return
}
// Otherwise, we are testing that Alice commits the preimages after
// receiving a CommitSig from Bob. Bob's commitment should now have 0
// HTLCs.
ctx.sendCommitSigBobToAlice(0)
// Since Alice will process the CommitSig asynchronously, we wait until
// she replies with her RevokeAndAck to ensure the tests reliably
// inspect her cache after advancing her state.
select {
// Received Alice's RevokeAndAck, assert that she has written all of the
// uncommitted preimages learned in this commitment.
case <-aliceMsgs:
checkHasPreimages(t, coreLink, htlcs, true)
// Alice didn't send her RevokeAndAck, something is wrong.
case <-time.After(15 * time.Second):
t.Fatalf("alice did not send her revocation")
}
}
// TestChannelLinkCleanupSpuriousResponses tests that we properly cleanup
// references in the event that internal retransmission continues as a result of
// not properly cleaning up Add/SettleFailRefs.
func TestChannelLinkCleanupSpuriousResponses(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, _, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
// Settle Alice in hodl ExitSettle mode so that she won't respond
// immediately to the htlc's meant for her. This allows us to control
// the responses she gives back to Bob.
coreLink.cfg.HodlMask = hodl.ExitSettle.Mask()
// Add two HTLCs to Alice's registry, that Bob can pay.
htlc1 := generateHtlc(t, coreLink, 0)
htlc2 := generateHtlc(t, coreLink, 1)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
// We start with he following scenario: Bob sends Alice two HTLCs, and a
// commitment dance ensures, leaving two HTLCs that Alice can respond
// to. Since Alice is in ExitSettle mode, we will then take over and
// provide targeted fail messages to test the link's ability to cleanup
// spurious responses.
//
// Bob Alice
// |------ add-1 ----->|
// |------ add-2 ----->|
// |------ sig ----->| commits add-1 + add-2
// |<----- rev ------|
// |<----- sig ------| commits add-1 + add-2
// |------ rev ----->|
ctx.sendHtlcBobToAlice(htlc1)
ctx.sendHtlcBobToAlice(htlc2)
ctx.sendCommitSigBobToAlice(2)
ctx.receiveRevAndAckAliceToBob()
ctx.receiveCommitSigAliceToBob(2)
ctx.sendRevAndAckBobToAlice()
// Give Alice to time to process the revocation.
time.Sleep(time.Second)
aliceFwdPkgs, err := coreLink.channel.LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load alice's fwdpkgs: %v", err)
}
// Alice should have exactly one forwarding package.
if len(aliceFwdPkgs) != 1 {
t.Fatalf("alice should have 1 fwd pkgs, has %d instead",
len(aliceFwdPkgs))
}
// We'll stash the height of these AddRefs, so that we can reconstruct
// the proper references later.
addHeight := aliceFwdPkgs[0].Height
// The first fwdpkg should have exactly 2 entries, one for each Add that
// was added during the last dance.
if aliceFwdPkgs[0].AckFilter.Count() != 2 {
t.Fatalf("alice fwdpkg should have 2 Adds, has %d instead",
aliceFwdPkgs[0].AckFilter.Count())
}
// Both of the entries in the FwdFilter should be unacked.
for i := 0; i < 2; i++ {
if aliceFwdPkgs[0].AckFilter.Contains(uint16(i)) {
t.Fatalf("alice fwdpkg index %d should not "+
"have ack", i)
}
}
// Now, construct a Fail packet for Bob settling the first HTLC. This
// packet will NOT include a sourceRef, meaning the AddRef on disk will
// not be acked after committing this response.
fail0 := &htlcPacket{
incomingChanID: bobChannel.ShortChanID(),
incomingHTLCID: 0,
obfuscator: NewMockObfuscator(),
htlc: &lnwire.UpdateFailHTLC{},
}
aliceLink.HandleSwitchPacket(fail0)
// Bob Alice
// |<----- fal-1 ------|
// |<----- sig ------| commits fal-1
ctx.receiveFailAliceToBob()
ctx.receiveCommitSigAliceToBob(1)
aliceFwdPkgs, err = coreLink.channel.LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load alice's fwdpkgs: %v", err)
}
// Alice should still only have one fwdpkg, as she hasn't yet received
// another revocation from Bob.
if len(aliceFwdPkgs) != 1 {
t.Fatalf("alice should have 1 fwd pkgs, has %d instead",
len(aliceFwdPkgs))
}
// Assert the fwdpkg still has 2 entries for the original Adds.
if aliceFwdPkgs[0].AckFilter.Count() != 2 {
t.Fatalf("alice fwdpkg should have 2 Adds, has %d instead",
aliceFwdPkgs[0].AckFilter.Count())
}
// Since the fail packet was missing the AddRef, the forward filter for
// either HTLC should not have been modified.
for i := 0; i < 2; i++ {
if aliceFwdPkgs[0].AckFilter.Contains(uint16(i)) {
t.Fatalf("alice fwdpkg index %d should not "+
"have ack", i)
}
}
// Complete the rest of the commitment dance, now that the forwarding
// packages have been verified.
//
// Bob Alice
// |------ rev ----->|
// |------ sig ----->|
// |<----- rev ------|
ctx.sendRevAndAckBobToAlice()
ctx.sendCommitSigBobToAlice(1)
ctx.receiveRevAndAckAliceToBob()
// Next, we'll construct a fail packet for add-2 (index 1), which we'll
// send to Bob and lock in. Since the AddRef is set on this instance, we
// should see the second HTLCs AddRef update the forward filter for the
// first fwd pkg.
fail1 := &htlcPacket{
sourceRef: &channeldb.AddRef{
Height: addHeight,
Index: 1,
},
incomingChanID: bobChannel.ShortChanID(),
incomingHTLCID: 1,
obfuscator: NewMockObfuscator(),
htlc: &lnwire.UpdateFailHTLC{},
}
aliceLink.HandleSwitchPacket(fail1)
// Bob Alice
// |<----- fal-1 ------|
// |<----- sig ------| commits fal-1
ctx.receiveFailAliceToBob()
ctx.receiveCommitSigAliceToBob(0)
aliceFwdPkgs, err = coreLink.channel.LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load alice's fwdpkgs: %v", err)
}
// Now that another commitment dance has completed, Alice should have 2
// forwarding packages.
if len(aliceFwdPkgs) != 2 {
t.Fatalf("alice should have 2 fwd pkgs, has %d instead",
len(aliceFwdPkgs))
}
// The most recent package should have no new HTLCs, so it should be
// empty.
if aliceFwdPkgs[1].AckFilter.Count() != 0 {
t.Fatalf("alice fwdpkg height=%d should have 0 Adds, "+
"has %d instead", aliceFwdPkgs[1].Height,
aliceFwdPkgs[1].AckFilter.Count())
}
// The index for the first AddRef should still be unacked, as the
// sourceRef was missing on the htlcPacket.
if aliceFwdPkgs[0].AckFilter.Contains(0) {
t.Fatalf("alice fwdpkg height=%d index=0 should not "+
"have an ack", aliceFwdPkgs[0].Height)
}
// The index for the second AddRef should now be acked, as it was
// properly constructed and committed in Alice's last commit sig.
if !aliceFwdPkgs[0].AckFilter.Contains(1) {
t.Fatalf("alice fwdpkg height=%d index=1 should have "+
"an ack", aliceFwdPkgs[0].Height)
}
// Complete the rest of the commitment dance.
//
// Bob Alice
// |------ rev ----->|
// |------ sig ----->|
// |<----- rev ------|
ctx.sendRevAndAckBobToAlice()
ctx.sendCommitSigBobToAlice(0)
ctx.receiveRevAndAckAliceToBob()
// We'll do a quick sanity check, and blindly send the same fail packet
// for the first HTLC. Since this HTLC index has already been settled,
// this should trigger an attempt to cleanup the spurious response.
// However, we expect it to result in a NOP since it is still missing
// its sourceRef.
aliceLink.HandleSwitchPacket(fail0)
// Allow the link enough time to process and reject the duplicate
// packet, we'll also check that this doesn't trigger Alice to send the
// fail to Bob.
select {
case <-aliceMsgs:
t.Fatalf("message sent for duplicate fail")
case <-time.After(time.Second):
}
aliceFwdPkgs, err = coreLink.channel.LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load alice's fwdpkgs: %v", err)
}
// Alice should now have 3 forwarding packages, and the latest should be
// empty.
if len(aliceFwdPkgs) != 3 {
t.Fatalf("alice should have 3 fwd pkgs, has %d instead",
len(aliceFwdPkgs))
}
if aliceFwdPkgs[2].AckFilter.Count() != 0 {
t.Fatalf("alice fwdpkg height=%d should have 0 Adds, "+
"has %d instead", aliceFwdPkgs[2].Height,
aliceFwdPkgs[2].AckFilter.Count())
}
// The state of the forwarding packages should be unmodified from the
// prior assertion, since the duplicate Fail for index 0 should have
// been ignored.
if aliceFwdPkgs[0].AckFilter.Contains(0) {
t.Fatalf("alice fwdpkg height=%d index=0 should not "+
"have an ack", aliceFwdPkgs[0].Height)
}
if !aliceFwdPkgs[0].AckFilter.Contains(1) {
t.Fatalf("alice fwdpkg height=%d index=1 should have "+
"an ack", aliceFwdPkgs[0].Height)
}
// Finally, construct a new Fail packet for the first HTLC, this time
// with the sourceRef properly constructed. When the link handles this
// duplicate, it should clean up the remaining AddRef state maintained
// in Alice's link, but it should not result in anything being sent to
// Bob.
fail0 = &htlcPacket{
sourceRef: &channeldb.AddRef{
Height: addHeight,
Index: 0,
},
incomingChanID: bobChannel.ShortChanID(),
incomingHTLCID: 0,
obfuscator: NewMockObfuscator(),
htlc: &lnwire.UpdateFailHTLC{},
}
aliceLink.HandleSwitchPacket(fail0)
// Allow the link enough time to process and reject the duplicate
// packet, we'll also check that this doesn't trigger Alice to send the
// fail to Bob.
select {
case <-aliceMsgs:
t.Fatalf("message sent for duplicate fail")
case <-time.After(time.Second):
}
aliceFwdPkgs, err = coreLink.channel.LoadFwdPkgs()
if err != nil {
t.Fatalf("unable to load alice's fwdpkgs: %v", err)
}
// Since no state transitions have been performed for the duplicate
// packets, Alice should still have the same 3 forwarding packages.
if len(aliceFwdPkgs) != 3 {
t.Fatalf("alice should have 3 fwd pkgs, has %d instead",
len(aliceFwdPkgs))
}
// Assert that all indices in our original forwarded have now been acked
// as a result of our spurious cleanup logic.
for i := 0; i < 2; i++ {
if !aliceFwdPkgs[0].AckFilter.Contains(uint16(i)) {
t.Fatalf("alice fwdpkg height=%d index=%d "+
"should have ack", aliceFwdPkgs[0].Height, i)
}
}
}
type mockPackager struct {
failLoadFwdPkgs bool
}
func (*mockPackager) AddFwdPkg(tx kvdb.RwTx, fwdPkg *channeldb.FwdPkg) error {
return nil
}
func (*mockPackager) SetFwdFilter(tx kvdb.RwTx, height uint64,
fwdFilter *channeldb.PkgFilter) error {
return nil
}
func (*mockPackager) AckAddHtlcs(tx kvdb.RwTx,
addRefs ...channeldb.AddRef) error {
return nil
}
func (m *mockPackager) LoadFwdPkgs(tx kvdb.RTx) ([]*channeldb.FwdPkg, error) {
if m.failLoadFwdPkgs {
return nil, fmt.Errorf("failing LoadFwdPkgs")
}
return nil, nil
}
func (*mockPackager) RemovePkg(tx kvdb.RwTx, height uint64) error {
return nil
}
func (*mockPackager) AckSettleFails(tx kvdb.RwTx,
settleFailRefs ...channeldb.SettleFailRef) error {
return nil
}
// TestChannelLinkFail tests that we will fail the channel, and force close the
// channel in certain situations.
func TestChannelLinkFail(t *testing.T) {
t.Parallel()
testCases := []struct {
// options is used to set up mocks and configure the link
// before it is started.
options func(*channelLink)
// link test is used to execute the given test on the channel
// link after it is started.
linkTest func(*testing.T, *channelLink, *lnwallet.LightningChannel)
// shouldForceClose indicates whether we expect the link to
// force close the channel in response to the actions performed
// during the linkTest.
shouldForceClose bool
// permanentFailure indicates whether we expect the link to
// consider the failure permanent in response to the actions
// performed during the linkTest.
permanentFailure bool
}{
{
// Test that we don't force close if syncing states
// fails at startup.
func(c *channelLink) {
c.cfg.SyncStates = true
// Make the syncChanStateCall fail by making
// the SendMessage call fail.
c.cfg.Peer.(*mockPeer).disconnected = true
},
func(t *testing.T, c *channelLink, _ *lnwallet.LightningChannel) {
// Should fail at startup.
},
false,
false,
},
{
// Test that we don't force closes the channel if
// resolving forward packages fails at startup.
func(c *channelLink) {
// We make the call to resolveFwdPkgs fail by
// making the underlying forwarder fail.
pkg := &mockPackager{
failLoadFwdPkgs: true,
}
c.channel.State().Packager = pkg
},
func(t *testing.T, c *channelLink, _ *lnwallet.LightningChannel) {
// Should fail at startup.
},
false,
false,
},
{
// Test that we force close the channel if we receive
// an invalid Settle message.
func(c *channelLink) {
},
func(t *testing.T, c *channelLink, _ *lnwallet.LightningChannel) {
// Recevive an htlc settle for an htlc that was
// never added.
htlcSettle := &lnwire.UpdateFulfillHTLC{
ID: 0,
PaymentPreimage: [32]byte{},
}
c.HandleChannelUpdate(htlcSettle)
},
true,
false,
},
{
// Test that we force close the channel if we receive
// an invalid CommitSig, not containing enough HTLC
// sigs.
func(c *channelLink) {
},
func(t *testing.T, c *channelLink, remoteChannel *lnwallet.LightningChannel) {
// Generate an HTLC and send to the link.
htlc1 := generateHtlc(t, c, 0)
ctx := linkTestContext{
t: t,
aliceLink: c,
bobChannel: remoteChannel,
}
ctx.sendHtlcBobToAlice(htlc1)
// Sign a commitment that will include
// signature for the HTLC just sent.
sig, htlcSigs, _, err :=
remoteChannel.SignNextCommitment()
if err != nil {
t.Fatalf("error signing commitment: %v",
err)
}
// Remove the HTLC sig, such that the commit
// sig will be invalid.
commitSig := &lnwire.CommitSig{
CommitSig: sig,
HtlcSigs: htlcSigs[1:],
}
c.HandleChannelUpdate(commitSig)
},
true,
false,
},
{
// Test that we force close the channel if we receive
// an invalid CommitSig, where the sig itself is
// corrupted.
func(c *channelLink) {
},
func(t *testing.T, c *channelLink, remoteChannel *lnwallet.LightningChannel) {
// Generate an HTLC and send to the link.
htlc1 := generateHtlc(t, c, 0)
ctx := linkTestContext{
t: t,
aliceLink: c,
bobChannel: remoteChannel,
}
ctx.sendHtlcBobToAlice(htlc1)
// Sign a commitment that will include
// signature for the HTLC just sent.
sig, htlcSigs, _, err :=
remoteChannel.SignNextCommitment()
if err != nil {
t.Fatalf("error signing commitment: %v",
err)
}
// Flip a bit on the signature, rendering it
// invalid.
sig[19] ^= 1
commitSig := &lnwire.CommitSig{
CommitSig: sig,
HtlcSigs: htlcSigs,
}
c.HandleChannelUpdate(commitSig)
},
true,
false,
},
{
// Test that we consider the failure permanent if we
// receive a link error from the remote.
func(c *channelLink) {
},
func(t *testing.T, c *channelLink, remoteChannel *lnwallet.LightningChannel) {
err := &lnwire.Error{}
c.HandleChannelUpdate(err)
},
false,
// TODO(halseth) For compatibility with CL we currently
// don't treat Errors as permanent errors.
false,
},
}
const chanAmt = btcutil.SatoshiPerBitcoin * 5
// Execute each test case.
for i, test := range testCases {
link, remoteChannel, _, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
coreLink := link.(*channelLink)
// Set up a channel used to check whether the link error
// force closed the channel.
linkErrors := make(chan LinkFailureError, 1)
coreLink.cfg.OnChannelFailure = func(_ lnwire.ChannelID,
_ lnwire.ShortChannelID, linkErr LinkFailureError) {
linkErrors <- linkErr
}
// Set up the link before starting it.
test.options(coreLink)
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
// Execute the test case.
test.linkTest(t, coreLink, remoteChannel)
// Currently we expect all test cases to lead to link error.
var linkErr LinkFailureError
select {
case linkErr = <-linkErrors:
case <-time.After(10 * time.Second):
t.Fatalf("%d) Alice did not fail"+
"channel", i)
}
// If we expect the link to force close the channel in this
// case, check that it happens. If not, make sure it does not
// happen.
if test.shouldForceClose != linkErr.ForceClose {
t.Fatalf("%d) Expected Alice to force close(%v), "+
"instead got(%v)", i, test.shouldForceClose,
linkErr.ForceClose)
}
if test.permanentFailure != linkErr.PermanentFailure {
t.Fatalf("%d) Expected Alice set permanent failure(%v), "+
"instead got(%v)", i, test.permanentFailure,
linkErr.PermanentFailure)
}
// Clean up before starting next test case.
cleanUp()
}
}
// TestExpectedFee tests calculation of ExpectedFee returns expected fee, given
// a baseFee, a feeRate, and an htlc amount.
func TestExpectedFee(t *testing.T) {
testCases := []struct {
baseFee lnwire.MilliSatoshi
feeRate lnwire.MilliSatoshi
htlcAmt lnwire.MilliSatoshi
expected lnwire.MilliSatoshi
}{
{
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(0),
},
{
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(1),
lnwire.MilliSatoshi(999999),
lnwire.MilliSatoshi(0),
},
{
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(1),
lnwire.MilliSatoshi(1000000),
lnwire.MilliSatoshi(1),
},
{
lnwire.MilliSatoshi(0),
lnwire.MilliSatoshi(1),
lnwire.MilliSatoshi(1000001),
lnwire.MilliSatoshi(1),
},
{
lnwire.MilliSatoshi(1),
lnwire.MilliSatoshi(1),
lnwire.MilliSatoshi(1000000),
lnwire.MilliSatoshi(2),
},
}
for _, test := range testCases {
f := ForwardingPolicy{
BaseFee: test.baseFee,
FeeRate: test.feeRate,
}
fee := ExpectedFee(f, test.htlcAmt)
if fee != test.expected {
t.Errorf("expected fee to be (%v), instead got (%v)", test.expected,
fee)
}
}
}
// TestForwardingAsymmetricTimeLockPolicies tests that each link is able to
// properly handle forwarding HTLCs when their outgoing channels have
// asymmetric policies w.r.t what they require for time locks.
func TestForwardingAsymmetricTimeLockPolicies(t *testing.T) {
t.Parallel()
// First, we'll create our traditional three hop network. Bob
// interacting with and asserting the state of two of the end points
// for this test.
channels, cleanUp, _, err := createClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5,
)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newThreeHopNetwork(
t, channels.aliceToBob, channels.bobToAlice, channels.bobToCarol,
channels.carolToBob, testStartingHeight,
)
if err := n.start(); err != nil {
t.Fatalf("unable to start three hop network: %v", err)
}
defer n.stop()
// Now that each of the links are up, we'll modify the link from Alice
// -> Bob to have a greater time lock delta than that of the link of
// Bob -> Carol.
newPolicy := n.firstBobChannelLink.cfg.FwrdingPolicy
newPolicy.TimeLockDelta = 7
n.firstBobChannelLink.UpdateForwardingPolicy(newPolicy)
// Now that the Alice -> Bob link has been updated, we'll craft and
// send a payment from Alice -> Carol. This should succeed as normal,
// even though Bob has asymmetric time lock policies.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(
amount, testStartingHeight, n.firstBobChannelLink,
n.carolChannelLink,
)
firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if err != nil {
t.Fatalf("unable to send payment: %v", err)
}
}
// TestCheckHtlcForward tests that a link is properly enforcing the HTLC
// forwarding policy.
func TestCheckHtlcForward(t *testing.T) {
fetchLastChannelUpdate := func(lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) {
return &lnwire.ChannelUpdate{}, nil
}
testChannel, _, fCleanUp, err := createTestChannel(
alicePrivKey, bobPrivKey, 100000, 100000,
1000, 1000, lnwire.ShortChannelID{},
)
if err != nil {
t.Fatal(err)
}
defer fCleanUp()
link := channelLink{
cfg: ChannelLinkConfig{
FwrdingPolicy: ForwardingPolicy{
TimeLockDelta: 20,
MinHTLCOut: 500,
MaxHTLC: 1000,
BaseFee: 10,
},
FetchLastChannelUpdate: fetchLastChannelUpdate,
MaxOutgoingCltvExpiry: DefaultMaxOutgoingCltvExpiry,
HtlcNotifier: &mockHTLCNotifier{},
},
log: log,
channel: testChannel.channel,
}
var hash [32]byte
t.Run("satisfied", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 0)
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
})
t.Run("below minhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 100, 50,
200, 150, 0)
if _, ok := result.WireMessage().(*lnwire.FailAmountBelowMinimum); !ok {
t.Fatalf("expected FailAmountBelowMinimum failure code")
}
})
t.Run("above maxhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1200,
200, 150, 0)
if _, ok := result.WireMessage().(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected FailTemporaryChannelFailure failure code")
}
})
t.Run("insufficient fee", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1005, 1000,
200, 150, 0)
if _, ok := result.WireMessage().(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
})
t.Run("expiry too soon", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 190)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooSoon); !ok {
t.Fatalf("expected FailExpiryTooSoon failure code")
}
})
t.Run("incorrect cltv expiry", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 190, 0)
if _, ok := result.WireMessage().(*lnwire.FailIncorrectCltvExpiry); !ok {
t.Fatalf("expected FailIncorrectCltvExpiry failure code")
}
})
t.Run("cltv expiry too far in the future", func(t *testing.T) {
// Check that expiry isn't too far in the future.
result := link.CheckHtlcForward(hash, 1500, 1000,
10200, 10100, 0)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooFar); !ok {
t.Fatalf("expected FailExpiryTooFar failure code")
}
})
}
// TestChannelLinkCanceledInvoice in this test checks the interaction
// between Alice and Bob for a canceled invoice.
func TestChannelLinkCanceledInvoice(t *testing.T) {
t.Parallel()
// Setup a alice-bob network.
alice, bob, cleanUp, err := createTwoClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
defer cleanUp()
n := newTwoHopNetwork(t, alice.channel, bob.channel, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
defer n.stop()
// Prepare an alice -> bob payment.
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(amount, testStartingHeight,
n.bobChannelLink)
firstHop := n.bobChannelLink.ShortChanID()
invoice, payFunc, err := preparePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
)
if err != nil {
t.Fatalf("unable to prepare the payment: %v", err)
}
// Cancel the invoice at bob's end.
hash := invoice.Terms.PaymentPreimage.Hash()
err = n.bobServer.registry.CancelInvoice(hash)
if err != nil {
t.Fatal(err)
}
// Have Alice fire the payment.
err = waitForPayFuncResult(payFunc, 30*time.Second)
// Because the invoice is canceled, we expect an unknown payment hash
// result.
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected ClearTextError, but got %v", err)
}
_, ok = rtErr.WireMessage().(*lnwire.FailIncorrectDetails)
if !ok {
t.Fatalf("expected unknown payment hash, but got %v", err)
}
}
type hodlInvoiceTestCtx struct {
n *twoHopNetwork
startBandwidthAlice lnwire.MilliSatoshi
startBandwidthBob lnwire.MilliSatoshi
hash lntypes.Hash
preimage lntypes.Preimage
amount lnwire.MilliSatoshi
errChan chan error
restoreBob func() (*lnwallet.LightningChannel, error)
cleanUp func()
}
func newHodlInvoiceTestCtx(t *testing.T) (*hodlInvoiceTestCtx, error) {
// Setup a alice-bob network.
alice, bob, cleanUp, err := createTwoClusterChannels(
btcutil.SatoshiPerBitcoin*3,
btcutil.SatoshiPerBitcoin*5,
)
if err != nil {
t.Fatalf("unable to create channel: %v", err)
}
n := newTwoHopNetwork(t, alice.channel, bob.channel, testStartingHeight)
if err := n.start(); err != nil {
t.Fatal(err)
}
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
bobBandwidthBefore := n.bobChannelLink.Bandwidth()
debug := false
if debug {
// Log message that alice receives.
n.aliceServer.intersect(
createLogFunc("alice", n.aliceChannelLink.ChanID()),
)
// Log message that bob receives.
n.bobServer.intersect(
createLogFunc("bob", n.bobChannelLink.ChanID()),
)
}
amount := lnwire.NewMSatFromSatoshis(btcutil.SatoshiPerBitcoin)
htlcAmt, totalTimelock, hops := generateHops(
amount, testStartingHeight, n.bobChannelLink,
)
// Generate hold invoice preimage.
r, err := generateRandomBytes(sha256.Size)
if err != nil {
t.Fatal(err)
}
preimage, err := lntypes.MakePreimage(r)
if err != nil {
t.Fatal(err)
}
hash := preimage.Hash()
// Have alice pay the hodl invoice, wait for bob's commitment state to
// be updated and the invoice state to be updated.
receiver := n.bobServer
receiver.registry.settleChan = make(chan lntypes.Hash)
firstHop := n.bobChannelLink.ShortChanID()
errChan := n.makeHoldPayment(
n.aliceServer, receiver, firstHop, hops, amount, htlcAmt,
totalTimelock, preimage,
)
select {
case err := <-errChan:
t.Fatalf("no payment result expected: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("timeout")
case h := <-receiver.registry.settleChan:
if hash != h {
t.Fatal("unexpect invoice settled")
}
}
return &hodlInvoiceTestCtx{
n: n,
startBandwidthAlice: aliceBandwidthBefore,
startBandwidthBob: bobBandwidthBefore,
preimage: preimage,
hash: hash,
amount: amount,
errChan: errChan,
restoreBob: bob.restore,
cleanUp: func() {
cleanUp()
n.stop()
},
}, nil
}
// TestChannelLinkHoldInvoiceSettle asserts that a hodl invoice can be settled.
func TestChannelLinkHoldInvoiceSettle(t *testing.T) {
t.Parallel()
defer timeout(t)()
ctx, err := newHodlInvoiceTestCtx(t)
if err != nil {
t.Fatal(err)
}
defer ctx.cleanUp()
err = ctx.n.bobServer.registry.SettleHodlInvoice(ctx.preimage)
if err != nil {
t.Fatal(err)
}
// Wait for payment to succeed.
err = <-ctx.errChan
if err != nil {
t.Fatal(err)
}
// Wait for Alice to receive the revocation. This is needed
// because the settles are pipelined to the switch and otherwise
// the bandwidth won't be updated by the time Alice receives a
// response here.
time.Sleep(2 * time.Second)
if ctx.startBandwidthAlice-ctx.amount !=
ctx.n.aliceChannelLink.Bandwidth() {
t.Fatal("alice bandwidth should have decrease on payment " +
"amount")
}
if ctx.startBandwidthBob+ctx.amount !=
ctx.n.bobChannelLink.Bandwidth() {
t.Fatalf("bob bandwidth isn't match: expected %v, got %v",
ctx.startBandwidthBob+ctx.amount,
ctx.n.bobChannelLink.Bandwidth())
}
}
// TestChannelLinkHoldInvoiceSettle asserts that a hodl invoice can be canceled.
func TestChannelLinkHoldInvoiceCancel(t *testing.T) {
t.Parallel()
defer timeout(t)()
ctx, err := newHodlInvoiceTestCtx(t)
if err != nil {
t.Fatal(err)
}
defer ctx.cleanUp()
err = ctx.n.bobServer.registry.CancelInvoice(ctx.hash)
if err != nil {
t.Fatal(err)
}
// Wait for payment to succeed.
err = <-ctx.errChan
assertFailureCode(t, err, lnwire.CodeIncorrectOrUnknownPaymentDetails)
}
// TestChannelLinkHoldInvoiceRestart asserts hodl htlcs are held after blocks
// are mined and the link is restarted. The initial expiry checks should not
// apply to hodl htlcs after restart.
func TestChannelLinkHoldInvoiceRestart(t *testing.T) {
t.Parallel()
defer timeout(t)()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
)
// We'll start by creating a new link with our chanAmt (5 BTC). We will
// only be testing Alice's behavior, so the reference to Bob's channel
// state is unnecessary.
aliceLink, bobChannel, _, start, cleanUp, restore, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
alice := newPersistentLinkHarness(
t, aliceLink, nil, restore,
)
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = alice.coreLink
registry = coreLink.cfg.Registry.(*mockInvoiceRegistry)
)
registry.settleChan = make(chan lntypes.Hash)
htlc, invoice := generateHtlcAndInvoice(t, 0)
// Convert into a hodl invoice and save the preimage for later.
preimage := invoice.Terms.PaymentPreimage
invoice.Terms.PaymentPreimage = nil
invoice.HodlInvoice = true
// We must add the invoice to the registry, such that Alice
// expects this payment.
err = registry.AddInvoice(
*invoice, htlc.PaymentHash,
)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
ctx := linkTestContext{
t: t,
aliceLink: alice.link,
aliceMsgs: alice.msgs,
bobChannel: bobChannel,
}
// Lock in htlc paying the hodl invoice.
ctx.sendHtlcBobToAlice(htlc)
ctx.sendCommitSigBobToAlice(1)
ctx.receiveRevAndAckAliceToBob()
ctx.receiveCommitSigAliceToBob(1)
ctx.sendRevAndAckBobToAlice()
// We expect a call to the invoice registry to notify the arrival of the
// htlc.
<-registry.settleChan
// Increase block height. This height will be retrieved by the link
// after restart.
coreLink.cfg.Switch.bestHeight++
// Restart link.
alice.restart(false, false)
ctx.aliceLink = alice.link
ctx.aliceMsgs = alice.msgs
// Expect htlc to be reprocessed.
<-registry.settleChan
// Settle the invoice with the preimage.
err = registry.SettleHodlInvoice(*preimage)
if err != nil {
t.Fatalf("settle hodl invoice: %v", err)
}
// Expect alice to send a settle and commitsig message to bob.
ctx.receiveSettleAliceToBob()
ctx.receiveCommitSigAliceToBob(0)
// Stop the link
alice.link.Stop()
// Check that no unexpected messages were sent.
select {
case msg := <-alice.msgs:
t.Fatalf("did not expect message %T", msg)
default:
}
}
// TestChannelLinkRevocationWindowRegular asserts that htlcs paying to a regular
// invoice are settled even if the revocation window gets exhausted.
func TestChannelLinkRevocationWindowRegular(t *testing.T) {
t.Parallel()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
)
// We'll start by creating a new link with our chanAmt (5 BTC). We will
// only be testing Alice's behavior, so the reference to Bob's channel
// state is unnecessary.
aliceLink, bobChannel, _, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
defer aliceLink.Stop()
var (
coreLink = aliceLink.(*channelLink)
registry = coreLink.cfg.Registry.(*mockInvoiceRegistry)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
registry.settleChan = make(chan lntypes.Hash)
htlc1, invoice1 := generateHtlcAndInvoice(t, 0)
htlc2, invoice2 := generateHtlcAndInvoice(t, 1)
// We must add the invoice to the registry, such that Alice
// expects this payment.
err = registry.AddInvoice(*invoice1, htlc1.PaymentHash)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
err = registry.AddInvoice(*invoice2, htlc2.PaymentHash)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
// Lock in htlc 1 on both sides.
ctx.sendHtlcBobToAlice(htlc1)
ctx.sendCommitSigBobToAlice(1)
ctx.receiveRevAndAckAliceToBob()
ctx.receiveCommitSigAliceToBob(1)
ctx.sendRevAndAckBobToAlice()
// We expect a call to the invoice registry to notify the arrival of the
// htlc.
select {
case <-registry.settleChan:
case <-time.After(5 * time.Second):
t.Fatal("expected invoice to be settled")
}
// Expect alice to send a settle and commitsig message to bob. Bob does
// not yet send the revocation.
ctx.receiveSettleAliceToBob()
ctx.receiveCommitSigAliceToBob(0)
// Pay invoice 2.
ctx.sendHtlcBobToAlice(htlc2)
ctx.sendCommitSigBobToAlice(2)
ctx.receiveRevAndAckAliceToBob()
// At this point, Alice cannot send a new commit sig to bob because the
// revocation window is exhausted.
// Bob sends revocation and signs commit with htlc1 settled.
ctx.sendRevAndAckBobToAlice()
// After the revocation, it is again possible for Alice to send a commit
// sig with htlc2.
ctx.receiveCommitSigAliceToBob(1)
}
// TestChannelLinkRevocationWindowHodl asserts that htlcs paying to a hodl
// invoice are settled even if the revocation window gets exhausted.
func TestChannelLinkRevocationWindowHodl(t *testing.T) {
t.Parallel()
const (
chanAmt = btcutil.SatoshiPerBitcoin * 5
)
// We'll start by creating a new link with our chanAmt (5 BTC). We will
// only be testing Alice's behavior, so the reference to Bob's channel
// state is unnecessary.
aliceLink, bobChannel, batchTicker, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, 0)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
registry = coreLink.cfg.Registry.(*mockInvoiceRegistry)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
registry.settleChan = make(chan lntypes.Hash)
// Generate two invoice-htlc pairs.
htlc1, invoice1 := generateHtlcAndInvoice(t, 0)
htlc2, invoice2 := generateHtlcAndInvoice(t, 1)
// Convert into hodl invoices and save the preimages for later.
preimage1 := invoice1.Terms.PaymentPreimage
invoice1.Terms.PaymentPreimage = nil
invoice1.HodlInvoice = true
preimage2 := invoice2.Terms.PaymentPreimage
invoice2.Terms.PaymentPreimage = nil
invoice2.HodlInvoice = true
// We must add the invoices to the registry, such that Alice
// expects the payments.
err = registry.AddInvoice(*invoice1, htlc1.PaymentHash)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
err = registry.AddInvoice(*invoice2, htlc2.PaymentHash)
if err != nil {
t.Fatalf("unable to add invoice to registry: %v", err)
}
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
// Lock in htlc 1 on both sides.
ctx.sendHtlcBobToAlice(htlc1)
ctx.sendCommitSigBobToAlice(1)
ctx.receiveRevAndAckAliceToBob()
ctx.receiveCommitSigAliceToBob(1)
ctx.sendRevAndAckBobToAlice()
// We expect a call to the invoice registry to notify the arrival of
// htlc 1.
select {
case <-registry.settleChan:
case <-time.After(15 * time.Second):
t.Fatal("exit hop notification not received")
}
// Lock in htlc 2 on both sides.
ctx.sendHtlcBobToAlice(htlc2)
ctx.sendCommitSigBobToAlice(2)
ctx.receiveRevAndAckAliceToBob()
ctx.receiveCommitSigAliceToBob(2)
ctx.sendRevAndAckBobToAlice()
select {
case <-registry.settleChan:
case <-time.After(15 * time.Second):
t.Fatal("exit hop notification not received")
}
// Settle invoice 1 with the preimage.
err = registry.SettleHodlInvoice(*preimage1)
if err != nil {
t.Fatalf("settle hodl invoice: %v", err)
}
// Expect alice to send a settle and commitsig message to bob. Bob does
// not yet send the revocation.
ctx.receiveSettleAliceToBob()
ctx.receiveCommitSigAliceToBob(1)
// Settle invoice 2 with the preimage.
err = registry.SettleHodlInvoice(*preimage2)
if err != nil {
t.Fatalf("settle hodl invoice: %v", err)
}
// Expect alice to send a settle for htlc 2.
ctx.receiveSettleAliceToBob()
// At this point, Alice cannot send a new commit sig to bob because the
// revocation window is exhausted.
// Sleep to let timer(s) expire.
time.Sleep(time.Second)
// We don't expect a commitSig from Alice.
select {
case msg := <-aliceMsgs:
t.Fatalf("did not expect message %T", msg)
default:
}
// Bob sends revocation and signs commit with htlc 1 settled.
ctx.sendRevAndAckBobToAlice()
// Allow some time for it to be processed by the link.
time.Sleep(time.Second)
// Trigger the batch timer as this may trigger Alice to send a commit
// sig.
batchTicker <- time.Time{}
// After the revocation, it is again possible for Alice to send a commit
// sig no more htlcs. Bob acks the update.
ctx.receiveCommitSigAliceToBob(0)
ctx.sendRevAndAckBobToAlice()
// Bob updates his remote commit tx.
ctx.sendCommitSigBobToAlice(0)
ctx.receiveRevAndAckAliceToBob()
// Stop the link
aliceLink.Stop()
// Check that no unexpected messages were sent.
select {
case msg := <-aliceMsgs:
t.Fatalf("did not expect message %T", msg)
default:
}
}
// TestChannelLinkReceiveEmptySig tests the response of the link to receiving an
// empty commit sig. This should be tolerated, but we shouldn't send out an
// empty sig ourselves.
func TestChannelLinkReceiveEmptySig(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
defer cleanUp()
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
aliceMsgs: aliceMsgs,
bobChannel: bobChannel,
}
htlc, _ := generateHtlcAndInvoice(t, 0)
// First, send an Add from Alice to Bob.
ctx.sendHtlcAliceToBob(0, htlc)
ctx.receiveHtlcAliceToBob()
// Tick the batch ticker to trigger a commitsig from Alice->Bob.
select {
case batchTicker <- time.Now():
case <-time.After(5 * time.Second):
t.Fatalf("could not force commit sig")
}
// Make Bob send a CommitSig. Since Bob hasn't received Alice's sig, he
// cannot add the htlc to his remote tx yet. The commit sig that we
// force Bob to send will be empty. Note that this normally does not
// happen, because the link (which is not present for Bob in this test)
// check whether Bob actually owes a sig first.
ctx.sendCommitSigBobToAlice(0)
// Receive a CommitSig from Alice covering the htlc from above.
ctx.receiveCommitSigAliceToBob(1)
// Wait for RevokeAndAck Alice->Bob. Even though Bob sent an empty
// commit sig, Alice still needs to revoke the previous commitment tx.
ctx.receiveRevAndAckAliceToBob()
// Send RevokeAndAck Bob->Alice to ack the added htlc.
ctx.sendRevAndAckBobToAlice()
// We received an empty commit sig, we accepted it, but there is nothing
// new to sign for us.
// No other messages are expected.
ctx.assertNoMsgFromAlice(time.Second)
// Stop the link
aliceLink.Stop()
}
// TestPendingCommitTicker tests that a link will fail itself after a timeout if
// the commitment dance stalls out.
func TestPendingCommitTicker(t *testing.T) {
t.Parallel()
const chanAmt = btcutil.SatoshiPerBitcoin * 5
const chanReserve = btcutil.SatoshiPerBitcoin * 1
aliceLink, bobChannel, batchTicker, start, cleanUp, _, err :=
newSingleLinkTestHarness(chanAmt, chanReserve)
if err != nil {
t.Fatalf("unable to create link: %v", err)
}
var (
coreLink = aliceLink.(*channelLink)
aliceMsgs = coreLink.cfg.Peer.(*mockPeer).sentMsgs
)
coreLink.cfg.PendingCommitTicker = ticker.NewForce(time.Millisecond)
linkErrs := make(chan LinkFailureError)
coreLink.cfg.OnChannelFailure = func(_ lnwire.ChannelID,
_ lnwire.ShortChannelID, linkErr LinkFailureError) {
linkErrs <- linkErr
}
if err := start(); err != nil {
t.Fatalf("unable to start test harness: %v", err)
}
defer cleanUp()
ctx := linkTestContext{
t: t,
aliceLink: aliceLink,
bobChannel: bobChannel,
aliceMsgs: aliceMsgs,
}
// Send an HTLC from Alice to Bob, and signal the batch ticker to signa
// a commitment.
htlc, _ := generateHtlcAndInvoice(t, 0)
ctx.sendHtlcAliceToBob(0, htlc)
ctx.receiveHtlcAliceToBob()
batchTicker <- time.Now()
select {
case msg := <-aliceMsgs:
if _, ok := msg.(*lnwire.CommitSig); !ok {
t.Fatalf("expected CommitSig, got: %T", msg)
}
case <-time.After(time.Second):
t.Fatalf("alice did not send commit sig")
}
// Check that Alice hasn't failed.
select {
case linkErr := <-linkErrs:
t.Fatalf("link failed unexpectedly: %v", linkErr)
case <-time.After(50 * time.Millisecond):
}
// Without completing the dance, send another HTLC from Alice to Bob.
// Since the revocation window has been exhausted, we should see the
// link fail itself immediately due to the low pending commit timeout.
// In production this would be much longer, e.g. a minute.
htlc, _ = generateHtlcAndInvoice(t, 1)
ctx.sendHtlcAliceToBob(1, htlc)
ctx.receiveHtlcAliceToBob()
batchTicker <- time.Now()
// Assert that we get the expected link failure from Alice.
select {
case linkErr := <-linkErrs:
if linkErr.code != ErrRemoteUnresponsive {
t.Fatalf("error code mismatch, "+
"want: ErrRemoteUnresponsive, got: %v",
linkErr.code)
}
case <-time.After(time.Second):
t.Fatalf("did not receive failure")
}
}
// assertFailureCode asserts that an error is of type ClearTextError and that
// the failure code is as expected.
func assertFailureCode(t *testing.T, err error, code lnwire.FailCode) {
rtErr, ok := err.(ClearTextError)
if !ok {
t.Fatalf("expected ClearTextError but got %T", err)
}
if rtErr.WireMessage().Code() != code {
t.Fatalf("expected %v but got %v",
code, rtErr.WireMessage().Code())
}
}