lnwallet: add IsChannelClean method and related tests

Adds a method to the LightningChannel struct called IsChannelClean
that returns a boolean telling the caller whether the channel state
is clean or not. Clean in this case means there are no lingering
updates to be signed for, no HTLC's active on either sides commitment
transaction, and no pending commitments on either side. This can be
used for dynamic commitments or during a strict cooperative close
process that ensures atomicity of the channel.
This commit is contained in:
eugene committed 2021-09-13 13:29:43 -04:00
1 parent 777abe5ff0
commit 7621d7f902
2 files changed
+191

No files matched your search

+44
View File
@@ -4470,6 +4470,50 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
return nil
}
// IsChannelClean returns true if neither side has pending commitments, neither
// side has HTLC's, and all updates are locked in irrevocably. Internally, it
// utilizes the oweCommitment function by calling it for local and remote
// evaluation. We check if we have a pending commitment for our local state
// since this function may be called by sub-systems that are not the link (e.g.
// the rpcserver), and the ReceiveNewCommitment & RevokeCurrentCommitment calls
// are not atomic, even though link processing ensures no updates can happen in
// between.
func (lc *LightningChannel) IsChannelClean() bool {
lc.RLock()
defer lc.RUnlock()
// Check whether we have a pending commitment for our local state.
if lc.localCommitChain.hasUnackedCommitment() {
return false
}
// Check whether our counterparty has a pending commitment for their
// state.
if lc.remoteCommitChain.hasUnackedCommitment() {
return false
}
// We call ActiveHtlcs to ensure there are no HTLCs on either
// commitment.
if len(lc.channelState.ActiveHtlcs()) != 0 {
return false
}
// Now check that both local and remote commitments are signing the
// same updates.
if lc.oweCommitment(true) {
return false
}
if lc.oweCommitment(false) {
return false
}
// If we reached this point, the channel has no HTLCs and both
// commitments sign the same updates.
return true
}
// OweCommitment returns a boolean value reflecting whether we need to send
// out a commitment signature because there are outstanding local updates and/or
// updates in the local commit tx that aren't reflected in the remote commit tx
+147
View File
@@ -9669,3 +9669,150 @@ func TestMayAddOutgoingHtlcZeroValue(t *testing.T) {
require.NoError(t, aliceChannel.MayAddOutgoingHtlc())
require.NoError(t, bobChannel.MayAddOutgoingHtlc())
}
// TestIsChannelClean tests that IsChannelClean returns the expected values
// in different channel states.
func TestIsChannelClean(t *testing.T) {
t.Parallel()
aliceChannel, bobChannel, cleanUp, err := CreateTestChannels(
channeldb.ZeroHtlcTxFeeBit,
)
require.NoError(t, err)
defer cleanUp()
// Channel state should be clean at the start of the test.
assertCleanOrDirty(true, aliceChannel, bobChannel, t)
// Assert that neither side considers the channel clean when alice
// sends an htlc.
// ---add--->
htlc, preimage := createHTLC(0, lnwire.MilliSatoshi(5000000))
_, err = aliceChannel.AddHTLC(htlc, nil)
require.NoError(t, err)
_, err = bobChannel.ReceiveHTLC(htlc)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// Assert that the channel remains dirty until the HTLC is completely
// removed from both commitments.
// ---sig--->
aliceSig, aliceHtlcSigs, _, err := aliceChannel.SignNextCommitment()
require.NoError(t, err)
err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---rev---
bobRevocation, _, err := bobChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---sig---
bobSig, bobHtlcSigs, _, err := bobChannel.SignNextCommitment()
require.NoError(t, err)
err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// ---rev--->
aliceRevocation, _, err := aliceChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <--settle--
err = bobChannel.SettleHTLC(preimage, 0, nil, nil, nil)
require.NoError(t, err)
err = aliceChannel.ReceiveHTLCSettle(preimage, 0)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---sig---
bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
require.NoError(t, err)
err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// ---rev--->
aliceRevocation, _, err = aliceChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// ---sig--->
aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
require.NoError(t, err)
err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---rev---
bobRevocation, _, err = bobChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
require.NoError(t, err)
assertCleanOrDirty(true, aliceChannel, bobChannel, t)
// Now we check that update_fee is handled and state is dirty until it
// is completely locked in.
// ---fee--->
fee := chainfee.SatPerKWeight(333)
err = aliceChannel.UpdateFee(fee)
require.NoError(t, err)
err = bobChannel.ReceiveUpdateFee(fee)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// ---sig--->
aliceSig, aliceHtlcSigs, _, err = aliceChannel.SignNextCommitment()
require.NoError(t, err)
err = bobChannel.ReceiveNewCommitment(aliceSig, aliceHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---rev---
bobRevocation, _, err = bobChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// <---sig---
bobSig, bobHtlcSigs, _, err = bobChannel.SignNextCommitment()
require.NoError(t, err)
err = aliceChannel.ReceiveNewCommitment(bobSig, bobHtlcSigs)
require.NoError(t, err)
assertCleanOrDirty(false, aliceChannel, bobChannel, t)
// The state should finally be clean after alice sends her revocation.
// ---rev--->
aliceRevocation, _, err = aliceChannel.RevokeCurrentCommitment()
require.NoError(t, err)
_, _, _, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
require.NoError(t, err)
assertCleanOrDirty(true, aliceChannel, bobChannel, t)
}
// assertCleanOrDirty is a helper function that asserts that both channels are
// clean if clean is true, and dirty if clean is false.
func assertCleanOrDirty(clean bool, alice, bob *LightningChannel,
t *testing.T) {
t.Helper()
if clean {
require.True(t, alice.IsChannelClean())
require.True(t, bob.IsChannelClean())
return
}
require.False(t, alice.IsChannelClean())
require.False(t, bob.IsChannelClean())
}