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:
2 files changed
+191
No files matched your search
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in new issue
Block a user