Merge pull request #3872 from joostjager/invalid-sig-fix

htlcswitch+lnwallet+channeldb: invalid sig fix
This commit is contained in:
Joost Jager 2020-01-23 21:08:07 +01:00 committed by GitHub
commit 1413995ab7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 553 additions and 39 deletions

@ -66,6 +66,11 @@ var (
// party. // party.
chanCommitmentKey = []byte("chan-commitment-key") chanCommitmentKey = []byte("chan-commitment-key")
// unsignedAckedUpdatesKey is an entry in the channel bucket that
// contains the remote updates that we have acked, but not yet signed
// for in one of our remote commits.
unsignedAckedUpdatesKey = []byte("unsigned-acked-updates-key")
// revocationStateKey stores their current revocation hash, our // revocationStateKey stores their current revocation hash, our
// preimage producer and their preimage store. // preimage producer and their preimage store.
revocationStateKey = []byte("revocation-state-key") revocationStateKey = []byte("revocation-state-key")
@ -1243,12 +1248,17 @@ func syncNewChannel(tx *bbolt.Tx, c *OpenChannel, addrs []net.Addr) error {
return putLinkNode(nodeInfoBucket, linkNode) return putLinkNode(nodeInfoBucket, linkNode)
} }
// UpdateCommitment updates the commitment state for the specified party // UpdateCommitment updates the local commitment state. It locks in the pending
// (remote or local). The commitment stat completely describes the balance // local updates that were received by us from the remote party. The commitment
// state at this point in the commitment chain. This method its to be called on // state completely describes the balance state at this point in the commitment
// two occasions: when we revoke our prior commitment state, and when the // chain. In addition to that, it persists all the remote log updates that we
// remote party revokes their prior commitment state. // have acked, but not signed a remote commitment for yet. These need to be
func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment) error { // persisted to be able to produce a valid commit signature if a restart would
// occur. This method its to be called when we revoke our prior commitment
// state.
func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment,
unsignedAckedUpdates []LogUpdate) error {
c.Lock() c.Lock()
defer c.Unlock() defer c.Unlock()
@ -1291,6 +1301,20 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment) error {
"revocations: %v", err) "revocations: %v", err)
} }
// Persist unsigned but acked remote updates that need to be
// restored after a restart.
var b bytes.Buffer
err = serializeLogUpdates(&b, unsignedAckedUpdates)
if err != nil {
return err
}
err = chanBucket.Put(unsignedAckedUpdatesKey, b.Bytes())
if err != nil {
return fmt.Errorf("unable to store dangline remote "+
"updates: %v", err)
}
return nil return nil
}) })
if err != nil { if err != nil {
@ -1568,6 +1592,42 @@ type CommitDiff struct {
SettleFailAcks []SettleFailRef SettleFailAcks []SettleFailRef
} }
// serializeLogUpdates serializes provided list of updates to a stream.
func serializeLogUpdates(w io.Writer, logUpdates []LogUpdate) error {
numUpdates := uint16(len(logUpdates))
if err := binary.Write(w, byteOrder, numUpdates); err != nil {
return err
}
for _, diff := range logUpdates {
err := WriteElements(w, diff.LogIndex, diff.UpdateMsg)
if err != nil {
return err
}
}
return nil
}
// deserializeLogUpdates deserializes a list of updates from a stream.
func deserializeLogUpdates(r io.Reader) ([]LogUpdate, error) {
var numUpdates uint16
if err := binary.Read(r, byteOrder, &numUpdates); err != nil {
return nil, err
}
logUpdates := make([]LogUpdate, numUpdates)
for i := 0; i < int(numUpdates); i++ {
err := ReadElements(r,
&logUpdates[i].LogIndex, &logUpdates[i].UpdateMsg,
)
if err != nil {
return nil, err
}
}
return logUpdates, nil
}
func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { func serializeCommitDiff(w io.Writer, diff *CommitDiff) error {
if err := serializeChanCommit(w, &diff.Commitment); err != nil { if err := serializeChanCommit(w, &diff.Commitment); err != nil {
return err return err
@ -1577,18 +1637,10 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error {
return err return err
} }
numUpdates := uint16(len(diff.LogUpdates)) if err := serializeLogUpdates(w, diff.LogUpdates); err != nil {
if err := binary.Write(w, byteOrder, numUpdates); err != nil {
return err return err
} }
for _, diff := range diff.LogUpdates {
err := WriteElements(w, diff.LogIndex, diff.UpdateMsg)
if err != nil {
return err
}
}
numOpenRefs := uint16(len(diff.OpenedCircuitKeys)) numOpenRefs := uint16(len(diff.OpenedCircuitKeys))
if err := binary.Write(w, byteOrder, numOpenRefs); err != nil { if err := binary.Write(w, byteOrder, numOpenRefs); err != nil {
return err return err
@ -1632,20 +1684,10 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) {
return nil, err return nil, err
} }
var numUpdates uint16 d.LogUpdates, err = deserializeLogUpdates(r)
if err := binary.Read(r, byteOrder, &numUpdates); err != nil {
return nil, err
}
d.LogUpdates = make([]LogUpdate, numUpdates)
for i := 0; i < int(numUpdates); i++ {
err := ReadElements(r,
&d.LogUpdates[i].LogIndex, &d.LogUpdates[i].UpdateMsg,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
var numOpenRefs uint16 var numOpenRefs uint16
if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil { if err := binary.Read(r, byteOrder, &numOpenRefs); err != nil {
@ -1737,6 +1779,14 @@ func (c *OpenChannel) AppendRemoteCommitChain(diff *CommitDiff) error {
return err return err
} }
// Clear unsigned acked remote updates. We are signing now for
// all that we've got.
err = chanBucket.Delete(unsignedAckedUpdatesKey)
if err != nil {
return fmt.Errorf("unable to clear dangling remote "+
"updates: %v", err)
}
// TODO(roasbeef): use seqno to derive key for later LCP // TODO(roasbeef): use seqno to derive key for later LCP
// With the bucket retrieved, we'll now serialize the commit // With the bucket retrieved, we'll now serialize the commit
@ -1790,6 +1840,38 @@ func (c *OpenChannel) RemoteCommitChainTip() (*CommitDiff, error) {
return cd, err return cd, err
} }
// UnsignedAckedUpdates retrieves the persisted unsigned acked remote log
// updates that still need to be signed for.
func (c *OpenChannel) UnsignedAckedUpdates() ([]LogUpdate, error) {
var updates []LogUpdate
err := c.Db.View(func(tx *bbolt.Tx) error {
chanBucket, err := fetchChanBucket(
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
)
switch err {
case nil:
case ErrNoChanDBExists, ErrNoActiveChannels, ErrChannelNotFound:
return nil
default:
return err
}
updateBytes := chanBucket.Get(unsignedAckedUpdatesKey)
if updateBytes == nil {
return nil
}
r := bytes.NewReader(updateBytes)
updates, err = deserializeLogUpdates(r)
return err
})
if err != nil {
return nil, err
}
return updates, nil
}
// InsertNextRevocation inserts the _next_ commitment point (revocation) into // InsertNextRevocation inserts the _next_ commitment point (revocation) into
// the database, and also modifies the internal RemoteNextRevocation attribute // the database, and also modifies the internal RemoteNextRevocation attribute
// to point to the passed key. This method is to be using during final channel // to point to the passed key. This method is to be using during final channel

@ -527,10 +527,34 @@ func TestChannelStateTransition(t *testing.T) {
// First update the local node's broadcastable state and also add a // First update the local node's broadcastable state and also add a
// CommitDiff remote node's as well in order to simulate a proper state // CommitDiff remote node's as well in order to simulate a proper state
// transition. // transition.
if err := channel.UpdateCommitment(&commitment); err != nil { unsignedAckedUpdates := []LogUpdate{
{
LogIndex: 2,
UpdateMsg: &lnwire.UpdateAddHTLC{
ChanID: lnwire.ChannelID{1, 2, 3},
},
},
}
err = channel.UpdateCommitment(&commitment, unsignedAckedUpdates)
if err != nil {
t.Fatalf("unable to update commitment: %v", err) t.Fatalf("unable to update commitment: %v", err)
} }
// Assert that update is correctly written to the database.
dbUnsignedAckedUpdates, err := channel.UnsignedAckedUpdates()
if err != nil {
t.Fatalf("unable to fetch dangling remote updates: %v", err)
}
if len(dbUnsignedAckedUpdates) != 1 {
t.Fatalf("unexpected number of dangling remote updates")
}
if !reflect.DeepEqual(
dbUnsignedAckedUpdates[0], unsignedAckedUpdates[0],
) {
t.Fatalf("unexpected update")
}
// The balances, new update, the HTLCs and the changes to the fake // The balances, new update, the HTLCs and the changes to the fake
// commitment transaction along with the modified signature should all // commitment transaction along with the modified signature should all
// have been updated. // have been updated.

@ -403,7 +403,7 @@ func TestRestoreChannelShells(t *testing.T) {
// Ensure that it isn't possible to modify the commitment state machine // Ensure that it isn't possible to modify the commitment state machine
// of this restored channel. // of this restored channel.
channel := nodeChans[0] channel := nodeChans[0]
err = channel.UpdateCommitment(nil) err = channel.UpdateCommitment(nil, nil)
if err != ErrNoRestoredChannelMutation { if err != ErrNoRestoredChannelMutation {
t.Fatalf("able to mutate restored channel") t.Fatalf("able to mutate restored channel")
} }

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"container/list" "container/list"
"crypto/sha256" "crypto/sha256"
"errors"
"fmt" "fmt"
"math" "math"
"sort" "sort"
@ -1059,6 +1060,13 @@ func (u *updateLog) appendUpdate(pd *PaymentDescriptor) {
u.logIndex++ u.logIndex++
} }
// restoreUpdate appends a new update to the tip of the updateLog. The entry is
// also added to index accordingly. This function differs from appendUpdate in
// that it won't increment the log index counter.
func (u *updateLog) restoreUpdate(pd *PaymentDescriptor) {
u.updateIndex[u.logIndex] = u.PushBack(pd)
}
// appendHtlc appends a new HTLC offer to the tip of the update log. The entry // appendHtlc appends a new HTLC offer to the tip of the update log. The entry
// is also added to the offer index accordingly. // is also added to the offer index accordingly.
func (u *updateLog) appendHtlc(pd *PaymentDescriptor) { func (u *updateLog) appendHtlc(pd *PaymentDescriptor) {
@ -1421,6 +1429,7 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
pd = &PaymentDescriptor{ pd = &PaymentDescriptor{
Amount: ogHTLC.Amount, Amount: ogHTLC.Amount,
RHash: ogHTLC.RHash,
RPreimage: wireMsg.PaymentPreimage, RPreimage: wireMsg.PaymentPreimage,
LogIndex: logUpdate.LogIndex, LogIndex: logUpdate.LogIndex,
ParentIndex: ogHTLC.HtlcIndex, ParentIndex: ogHTLC.HtlcIndex,
@ -1483,6 +1492,110 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
return pd, nil return pd, nil
} }
// remoteLogUpdateToPayDesc converts a LogUpdate into a matching
// PaymentDescriptor entry that can be re-inserted into the update log. This
// method is used when we revoked a local commitment, but the connection was
// obstructed before we could sign a remote commitment that contains these
// updates. In this case, we need to re-insert the original entries back into
// the update log so we can resume as if nothing happened. The height of the
// latest local commitment is also expected to be provided. We are restoring all
// log update entries with this height, even though the real commitment height
// may be lower. In the way these fields are used elsewhere, this doesn't change
// anything.
func (lc *LightningChannel) remoteLogUpdateToPayDesc(logUpdate *channeldb.LogUpdate,
localUpdateLog *updateLog, commitHeight uint64) (*PaymentDescriptor,
error) {
switch wireMsg := logUpdate.UpdateMsg.(type) {
case *lnwire.UpdateAddHTLC:
pd := &PaymentDescriptor{
RHash: wireMsg.PaymentHash,
Timeout: wireMsg.Expiry,
Amount: wireMsg.Amount,
EntryType: Add,
HtlcIndex: wireMsg.ID,
LogIndex: logUpdate.LogIndex,
addCommitHeightLocal: commitHeight,
}
pd.OnionBlob = make([]byte, len(wireMsg.OnionBlob))
copy(pd.OnionBlob, wireMsg.OnionBlob[:])
// We don't need to generate an htlc script yet. This will be
// done once we sign our remote commitment.
return pd, nil
// For HTLCs that the remote party settled, we'll fetch the original
// offered HTLC from the local update log so we can retrieve the same
// PaymentDescriptor that ReceiveHTLCSettle would produce.
case *lnwire.UpdateFulfillHTLC:
ogHTLC := localUpdateLog.lookupHtlc(wireMsg.ID)
return &PaymentDescriptor{
Amount: ogHTLC.Amount,
RHash: ogHTLC.RHash,
RPreimage: wireMsg.PaymentPreimage,
LogIndex: logUpdate.LogIndex,
ParentIndex: ogHTLC.HtlcIndex,
EntryType: Settle,
removeCommitHeightLocal: commitHeight,
}, nil
// If we received a failure for a prior outgoing HTLC, then we'll
// consult the local update log so we can retrieve the information of
// the original HTLC we're failing.
case *lnwire.UpdateFailHTLC:
ogHTLC := localUpdateLog.lookupHtlc(wireMsg.ID)
return &PaymentDescriptor{
Amount: ogHTLC.Amount,
RHash: ogHTLC.RHash,
ParentIndex: ogHTLC.HtlcIndex,
LogIndex: logUpdate.LogIndex,
EntryType: Fail,
FailReason: wireMsg.Reason[:],
removeCommitHeightLocal: commitHeight,
}, nil
// HTLC fails due to malformed onion blobs are treated the exact same
// way as regular HTLC fails.
case *lnwire.UpdateFailMalformedHTLC:
ogHTLC := localUpdateLog.lookupHtlc(wireMsg.ID)
return &PaymentDescriptor{
Amount: ogHTLC.Amount,
RHash: ogHTLC.RHash,
ParentIndex: ogHTLC.HtlcIndex,
LogIndex: logUpdate.LogIndex,
EntryType: MalformedFail,
FailCode: wireMsg.FailureCode,
ShaOnionBlob: wireMsg.ShaOnionBlob,
removeCommitHeightLocal: commitHeight,
}, nil
// For fee updates we'll create a FeeUpdate type to add to the log. We
// reuse the amount field to hold the fee rate. Since the amount field
// is denominated in msat we won't lose precision when storing the
// sat/kw denominated feerate. Note that we set both the add and remove
// height to the same value, as we consider the fee update locked in by
// adding and removing it at the same height.
case *lnwire.UpdateFee:
return &PaymentDescriptor{
LogIndex: logUpdate.LogIndex,
Amount: lnwire.NewMSatFromSatoshis(
btcutil.Amount(wireMsg.FeePerKw),
),
EntryType: FeeUpdate,
addCommitHeightLocal: commitHeight,
removeCommitHeightLocal: commitHeight,
}, nil
default:
return nil, errors.New("unknown message type")
}
}
// restoreCommitState will restore the local commitment chain and updateLog // restoreCommitState will restore the local commitment chain and updateLog
// state to a consistent in-memory representation of the passed disk commitment. // state to a consistent in-memory representation of the passed disk commitment.
// This method is to be used upon reconnection to our channel counter party. // This method is to be used upon reconnection to our channel counter party.
@ -1582,12 +1695,19 @@ func (lc *LightningChannel) restoreCommitState(
) )
} }
// Fetch remote updates that we have acked but not yet signed for.
unsignedAckedUpdates, err := lc.channelState.UnsignedAckedUpdates()
if err != nil {
return err
}
// Finally, with the commitment states restored, we'll now restore the // Finally, with the commitment states restored, we'll now restore the
// state logs based on the current local+remote commit, and any pending // state logs based on the current local+remote commit, and any pending
// remote commit that exists. // remote commit that exists.
err = lc.restoreStateLogs( err = lc.restoreStateLogs(
localCommit, remoteCommit, pendingRemoteCommit, localCommit, remoteCommit, pendingRemoteCommit,
pendingRemoteCommitDiff, pendingRemoteKeyChain, pendingRemoteCommitDiff, pendingRemoteKeyChain,
unsignedAckedUpdates,
) )
if err != nil { if err != nil {
return err return err
@ -1603,7 +1723,8 @@ func (lc *LightningChannel) restoreCommitState(
func (lc *LightningChannel) restoreStateLogs( func (lc *LightningChannel) restoreStateLogs(
localCommitment, remoteCommitment, pendingRemoteCommit *commitment, localCommitment, remoteCommitment, pendingRemoteCommit *commitment,
pendingRemoteCommitDiff *channeldb.CommitDiff, pendingRemoteCommitDiff *channeldb.CommitDiff,
pendingRemoteKeys *CommitmentKeyRing) error { pendingRemoteKeys *CommitmentKeyRing,
unsignedAckedUpdates []channeldb.LogUpdate) error {
// We make a map of incoming HTLCs to the height of the remote // We make a map of incoming HTLCs to the height of the remote
// commitment they were first added, and outgoing HTLCs to the height // commitment they were first added, and outgoing HTLCs to the height
@ -1669,11 +1790,90 @@ func (lc *LightningChannel) restoreStateLogs(
lc.localUpdateLog.restoreHtlc(&htlc) lc.localUpdateLog.restoreHtlc(&htlc)
} }
// If we didn't have a dangling (un-acked) commit for the remote party, // If we have a dangling (un-acked) commit for the remote party, then we
// then we can exit here. // restore the updates leading up to this commit.
if pendingRemoteCommit == nil { if pendingRemoteCommit != nil {
return nil err := lc.restorePendingLocalUpdates(
pendingRemoteCommitDiff, pendingRemoteKeys,
)
if err != nil {
return err
} }
}
// Restore unsigned acked remote log updates so that we can include them
// in our next signature.
err := lc.restorePendingRemoteUpdates(
unsignedAckedUpdates, localCommitment.height,
)
if err != nil {
return err
}
return nil
}
// restorePendingRemoteUpdates restores the acked remote log updates that we
// haven't yet signed for.
func (lc *LightningChannel) restorePendingRemoteUpdates(
unsignedAckedUpdates []channeldb.LogUpdate,
localCommitmentHeight uint64) error {
lc.log.Debugf("Restoring %v dangling remote updates",
len(unsignedAckedUpdates))
for _, logUpdate := range unsignedAckedUpdates {
logUpdate := logUpdate
payDesc, err := lc.remoteLogUpdateToPayDesc(
&logUpdate, lc.localUpdateLog, localCommitmentHeight,
)
if err != nil {
return err
}
// Sanity check that we are not restoring a remote log update
// that we haven't received a sig for.
if payDesc.LogIndex >= lc.remoteUpdateLog.logIndex {
return fmt.Errorf("attempted to restore an "+
"unsigned remote update: log_index=%v",
payDesc.LogIndex)
}
// Insert the update into the log. The log update index doesn't
// need to be incremented (hence the restore calls), because its
// final value was properly persisted with the last local
// commitment update.
switch payDesc.EntryType {
case Add:
lc.remoteUpdateLog.restoreHtlc(payDesc)
// Sanity check to be sure that we are not restoring an
// add update that the remote hasn't signed for yet.
if payDesc.HtlcIndex >= lc.remoteUpdateLog.htlcCounter {
return fmt.Errorf("attempted to restore an "+
"unsigned remote htlc: htlc_index=%v",
payDesc.HtlcIndex)
}
case FeeUpdate:
lc.remoteUpdateLog.restoreUpdate(payDesc)
default:
lc.remoteUpdateLog.restoreUpdate(payDesc)
lc.localUpdateLog.markHtlcModified(payDesc.ParentIndex)
}
}
return nil
}
// restorePendingLocalUpdates restores the local log updates leading up to the
// given pending remote commitment.
func (lc *LightningChannel) restorePendingLocalUpdates(
pendingRemoteCommitDiff *channeldb.CommitDiff,
pendingRemoteKeys *CommitmentKeyRing) error {
pendingCommit := pendingRemoteCommitDiff.Commitment pendingCommit := pendingRemoteCommitDiff.Commitment
pendingHeight := pendingCommit.CommitHeight pendingHeight := pendingCommit.CommitHeight
@ -2781,6 +2981,96 @@ func (lc *LightningChannel) createCommitDiff(
}, nil }, nil
} }
// getUnsignedAckedUpdates returns all remote log updates that we haven't
// signed for yet ourselves.
func (lc *LightningChannel) getUnsignedAckedUpdates() []channeldb.LogUpdate {
// First, we need to convert the funding outpoint into the ID that's
// used on the wire to identify this channel.
chanID := lnwire.NewChanIDFromOutPoint(&lc.channelState.FundingOutpoint)
// Fetch the last remote update that we have signed for.
lastRemoteCommitted := lc.remoteCommitChain.tip().theirMessageIndex
// Fetch the last remote update that we have acked.
lastLocalCommitted := lc.localCommitChain.tail().theirMessageIndex
// We'll now run through the remote update log to locate the items that
// we haven't signed for yet. This will be the set of items we need to
// restore if we reconnect in order to produce the signature that the
// remote party expects.
var logUpdates []channeldb.LogUpdate
for e := lc.remoteUpdateLog.Front(); e != nil; e = e.Next() {
pd := e.Value.(*PaymentDescriptor)
// Skip all remote updates that we have already included in our
// commit chain.
if pd.LogIndex < lastRemoteCommitted {
continue
}
// Skip all remote updates that we haven't acked yet. At the
// moment this function is called, there shouldn't be any, but
// we check it anyway to make this function more generally
// usable.
if pd.LogIndex >= lastLocalCommitted {
continue
}
logUpdate := channeldb.LogUpdate{
LogIndex: pd.LogIndex,
}
// We'll map the type of the PaymentDescriptor to one of the
// four messages that it corresponds to.
switch pd.EntryType {
case Add:
htlc := &lnwire.UpdateAddHTLC{
ChanID: chanID,
ID: pd.HtlcIndex,
Amount: pd.Amount,
Expiry: pd.Timeout,
PaymentHash: pd.RHash,
}
copy(htlc.OnionBlob[:], pd.OnionBlob)
logUpdate.UpdateMsg = htlc
case Settle:
logUpdate.UpdateMsg = &lnwire.UpdateFulfillHTLC{
ChanID: chanID,
ID: pd.ParentIndex,
PaymentPreimage: pd.RPreimage,
}
case Fail:
logUpdate.UpdateMsg = &lnwire.UpdateFailHTLC{
ChanID: chanID,
ID: pd.ParentIndex,
Reason: pd.FailReason,
}
case MalformedFail:
logUpdate.UpdateMsg = &lnwire.UpdateFailMalformedHTLC{
ChanID: chanID,
ID: pd.ParentIndex,
ShaOnionBlob: pd.ShaOnionBlob,
FailureCode: pd.FailCode,
}
case FeeUpdate:
// The Amount field holds the feerate denominated in
// msat. Since feerates are only denominated in sat/kw,
// we can convert it without loss of precision.
logUpdate.UpdateMsg = &lnwire.UpdateFee{
ChanID: chanID,
FeePerKw: uint32(pd.Amount.ToSatoshis()),
}
}
logUpdates = append(logUpdates, logUpdate)
}
return logUpdates
}
// validateCommitmentSanity is used to validate the current state of the // validateCommitmentSanity is used to validate the current state of the
// commitment transaction in terms of the ChannelConstraints that we and our // commitment transaction in terms of the ChannelConstraints that we and our
// remote peer agreed upon during the funding workflow. The predictAdded // remote peer agreed upon during the funding workflow. The predictAdded
@ -3970,15 +4260,24 @@ func (lc *LightningChannel) RevokeCurrentCommitment() (*lnwire.RevokeAndAck, []c
// persistent storage. // persistent storage.
chainTail := lc.localCommitChain.tail() chainTail := lc.localCommitChain.tail()
newCommitment := chainTail.toDiskCommit(true) newCommitment := chainTail.toDiskCommit(true)
err = lc.channelState.UpdateCommitment(newCommitment)
// Get the unsigned acked remotes updates that are currently in memory.
// We need them after a restart to sync our remote commitment with what
// is committed locally.
unsignedAckedUpdates := lc.getUnsignedAckedUpdates()
err = lc.channelState.UpdateCommitment(
newCommitment, unsignedAckedUpdates,
)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
lc.log.Tracef("state transition accepted: "+ lc.log.Tracef("state transition accepted: "+
"our_balance=%v, their_balance=%v", "our_balance=%v, their_balance=%v, unsigned_acked_updates=%v",
chainTail.ourBalance, chainTail.ourBalance,
chainTail.theirBalance) chainTail.theirBalance,
len(unsignedAckedUpdates))
revocationMsg.ChanID = lnwire.NewChanIDFromOutPoint( revocationMsg.ChanID = lnwire.NewChanIDFromOutPoint(
&lc.channelState.FundingOutpoint, &lc.channelState.FundingOutpoint,

@ -19,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )
@ -3050,6 +3051,112 @@ func TestChanSyncOweCommitment(t *testing.T) {
} }
} }
// TestChanSyncOweCommitmentPendingRemote asserts that local updates are applied
// to the remote commit across restarts.
func TestChanSyncOweCommitmentPendingRemote(t *testing.T) {
t.Parallel()
// Create a test channel which will be used for the duration of this
// unittest.
aliceChannel, bobChannel, cleanUp, err := CreateTestChannels(true)
if err != nil {
t.Fatalf("unable to create test channels: %v", err)
}
defer cleanUp()
var fakeOnionBlob [lnwire.OnionPacketSize]byte
copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
// We'll start off the scenario where Bob send two htlcs to Alice in a
// single state update.
var preimages []lntypes.Preimage
const numHtlcs = 2
for id := byte(0); id < numHtlcs; id++ {
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
var bobPreimage [32]byte
copy(bobPreimage[:], bytes.Repeat([]byte{id}, 32))
rHash := sha256.Sum256(bobPreimage[:])
h := &lnwire.UpdateAddHTLC{
PaymentHash: rHash,
Amount: htlcAmt,
Expiry: uint32(10),
OnionBlob: fakeOnionBlob,
}
htlcIndex, err := bobChannel.AddHTLC(h, nil)
if err != nil {
t.Fatalf("unable to add bob's htlc: %v", err)
}
h.ID = htlcIndex
if _, err := aliceChannel.ReceiveHTLC(h); err != nil {
t.Fatalf("unable to recv bob's htlc: %v", err)
}
preimages = append(preimages, bobPreimage)
}
// With the HTLCs applied to both update logs, we'll initiate a state
// transition from Bob.
if err := ForceStateTransition(bobChannel, aliceChannel); err != nil {
t.Fatalf("unable to complete bob's state transition: %v", err)
}
// Next, Alice settles the HTLCs from Bob in distinct state updates.
for i := 0; i < numHtlcs; i++ {
err = aliceChannel.SettleHTLC(preimages[i], uint64(i), nil, nil, nil)
if err != nil {
t.Fatalf("unable to settle htlc: %v", err)
}
err = bobChannel.ReceiveHTLCSettle(preimages[i], uint64(i))
if err != nil {
t.Fatalf("unable to settle htlc: %v", err)
}
aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
if err != nil {
t.Fatalf("unable to sign commitment: %v", err)
}
err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
if err != nil {
t.Fatalf("unable to receive commitment: %v", err)
}
// Bob revokes his current commitment. After this call
// completes, the htlc is settled on the local commitment
// transaction. Bob still owes Alice a signature to also settle
// the htlc on her local commitment transaction.
bobRevoke, _, err := bobChannel.RevokeCurrentCommitment()
if err != nil {
t.Fatalf("unable to revoke commitment: %v", err)
}
_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevoke)
if err != nil {
t.Fatalf("unable to revoke commitment: %v", err)
}
}
// We restart Bob. This should have no impact on further message that
// are generated.
bobChannel, err = restartChannel(bobChannel)
if err != nil {
t.Fatalf("unable to restart bob: %v", err)
}
// Bob signs the commitment he owes.
_, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
if err != nil {
t.Fatalf("unable to sign commitment: %v", err)
}
// This commitment is expected to contain no htlcs anymore.
if len(bobHtlcSigs) != 0 {
t.Fatalf("no htlcs expected, but got %v", len(bobHtlcSigs))
}
}
// TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before // TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before
// he receiver's Alice's RevokeAndAck message, then Alice concludes that she // he receiver's Alice's RevokeAndAck message, then Alice concludes that she
// needs to re-send the RevokeAndAck. After the revocation has been sent, both // needs to re-send the RevokeAndAck. After the revocation has been sent, both
@ -6178,9 +6285,11 @@ func TestChannelRestoreUpdateLogsFailedHTLC(t *testing.T) {
// At this point Alice has advanced her local commitment chain to a // At this point Alice has advanced her local commitment chain to a
// commitment with no HTLCs left. The current state on her remote // commitment with no HTLCs left. The current state on her remote
// commitment chain, however, still has the HTLC active, as she hasn't // commitment chain, however, still has the HTLC active, as she hasn't
// sent a new signature yet. // sent a new signature yet. If we'd now restart and restore, the htlc
// failure update should still be waiting for inclusion in Alice's next
// signature. Otherwise the produced signature would be invalid.
assertInLogs(t, aliceChannel, 1, 0, 0, 1) assertInLogs(t, aliceChannel, 1, 0, 0, 1)
restoreAndAssert(t, aliceChannel, 1, 0, 0, 0) restoreAndAssert(t, aliceChannel, 1, 0, 0, 1)
// Now send a signature from Alice. This will give Bob a new commitment // Now send a signature from Alice. This will give Bob a new commitment
// where the HTLC is removed. // where the HTLC is removed.