htlcswitch: implement flush and commit lifecycle hooks for channelLink
This commit is contained in:
1 parent
601afaf1b2
commit
64fda6ca65
4 files changed
+236
-41
No files matched your search
+160
-27
@@ -375,10 +375,72 @@ type channelLink struct {
|
|||||||
// UpdateAddHTLC.
|
// UpdateAddHTLC.
|
||||||
isIncomingAddBlocked atomic.Bool
|
isIncomingAddBlocked atomic.Bool
|
||||||
|
|
||||||
|
// flushHooks is a hookMap that is triggered when we reach a channel
|
||||||
|
// state with no live HTLCs.
|
||||||
|
flushHooks hookMap
|
||||||
|
|
||||||
|
// outgoingCommitHooks is a hookMap that is triggered after we send our
|
||||||
|
// next CommitSig.
|
||||||
|
outgoingCommitHooks hookMap
|
||||||
|
|
||||||
|
// incomingCommitHooks is a hookMap that is triggered after we receive
|
||||||
|
// our next CommitSig.
|
||||||
|
incomingCommitHooks hookMap
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hookMap is a data structure that is used to track the hooks that need to be
|
||||||
|
// called in various parts of the channelLink's lifecycle.
|
||||||
|
//
|
||||||
|
// WARNING: NOT thread-safe.
|
||||||
|
type hookMap struct {
|
||||||
|
// allocIdx keeps track of the next id we haven't yet allocated.
|
||||||
|
allocIdx atomic.Uint64
|
||||||
|
|
||||||
|
// transient is a map of hooks that are only called the next time invoke
|
||||||
|
// is called. These hooks are deleted during invoke.
|
||||||
|
transient map[uint64]func()
|
||||||
|
|
||||||
|
// newTransients is a channel that we use to accept new hooks into the
|
||||||
|
// hookMap.
|
||||||
|
newTransients chan func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHookMap initializes a new empty hookMap.
|
||||||
|
func newHookMap() hookMap {
|
||||||
|
return hookMap{
|
||||||
|
allocIdx: atomic.Uint64{},
|
||||||
|
transient: make(map[uint64]func()),
|
||||||
|
newTransients: make(chan func()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// alloc allocates space in the hook map for the supplied hook, the second
|
||||||
|
// argument determines whether it goes into the transient or persistent part
|
||||||
|
// of the hookMap.
|
||||||
|
func (m *hookMap) alloc(hook func()) uint64 {
|
||||||
|
// We assume we never overflow a uint64. Seems OK.
|
||||||
|
hookID := m.allocIdx.Add(1)
|
||||||
|
if hookID == 0 {
|
||||||
|
panic("hookMap allocIdx overflow")
|
||||||
|
}
|
||||||
|
m.transient[hookID] = hook
|
||||||
|
|
||||||
|
return hookID
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoke is used on a hook map to call all the registered hooks and then clear
|
||||||
|
// out the transient hooks so they are not called again.
|
||||||
|
func (m *hookMap) invoke() {
|
||||||
|
for _, hook := range m.transient {
|
||||||
|
hook()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.transient = make(map[uint64]func())
|
||||||
|
}
|
||||||
|
|
||||||
// hodlHtlc contains htlc data that is required for resolution.
|
// hodlHtlc contains htlc data that is required for resolution.
|
||||||
type hodlHtlc struct {
|
type hodlHtlc struct {
|
||||||
pd *lnwallet.PaymentDescriptor
|
pd *lnwallet.PaymentDescriptor
|
||||||
@@ -393,14 +455,17 @@ func NewChannelLink(cfg ChannelLinkConfig,
|
|||||||
logPrefix := fmt.Sprintf("ChannelLink(%v):", channel.ChannelPoint())
|
logPrefix := fmt.Sprintf("ChannelLink(%v):", channel.ChannelPoint())
|
||||||
|
|
||||||
return &channelLink{
|
return &channelLink{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
shortChanID: channel.ShortChanID(),
|
shortChanID: channel.ShortChanID(),
|
||||||
shutdownRequest: make(chan *shutdownReq),
|
shutdownRequest: make(chan *shutdownReq),
|
||||||
hodlMap: make(map[models.CircuitKey]hodlHtlc),
|
hodlMap: make(map[models.CircuitKey]hodlHtlc),
|
||||||
hodlQueue: queue.NewConcurrentQueue(10),
|
hodlQueue: queue.NewConcurrentQueue(10),
|
||||||
log: build.NewPrefixLog(logPrefix, log),
|
log: build.NewPrefixLog(logPrefix, log),
|
||||||
quit: make(chan struct{}),
|
flushHooks: newHookMap(),
|
||||||
|
outgoingCommitHooks: newHookMap(),
|
||||||
|
incomingCommitHooks: newHookMap(),
|
||||||
|
quit: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,20 +677,33 @@ func (l *channelLink) IsFlushing(linkDirection LinkDirection) bool {
|
|||||||
return l.isIncomingAddBlocked.Load()
|
return l.isIncomingAddBlocked.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnFlushedOnce adds a hook that will be called the next time the
|
// OnFlushedOnce adds a hook that will be called the next time the channel
|
||||||
// channel state reaches zero htlcs. This hook will only ever be called
|
// state reaches zero htlcs. This hook will only ever be called once. If the
|
||||||
// once. If the channel state already has zero htlcs, then this will be
|
// channel state already has zero htlcs, then this will be called immediately.
|
||||||
// called immediately.
|
func (l *channelLink) OnFlushedOnce(hook func()) {
|
||||||
func (l *channelLink) OnFlushedOnce(func()) {
|
select {
|
||||||
// TODO(proofofkeags): Implement
|
case l.flushHooks.newTransients <- hook:
|
||||||
|
case <-l.quit:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnCommitOnce adds a hook that will be called the next time a CommitSig
|
// OnCommitOnce adds a hook that will be called the next time a CommitSig
|
||||||
// message is sent in the argument's LinkDirection. This hook will only ever be
|
// message is sent in the argument's LinkDirection. This hook will only ever be
|
||||||
// called once. If no CommitSig is owed in the argument's LinkDirection, then
|
// called once. If no CommitSig is owed in the argument's LinkDirection, then
|
||||||
// we will call this hook immediately.
|
// we will call this hook be run immediately.
|
||||||
func (l *channelLink) OnCommitOnce(LinkDirection, func()) {
|
func (l *channelLink) OnCommitOnce(direction LinkDirection, hook func()) {
|
||||||
// TODO(proofofkeags): Implement
|
var queue chan func()
|
||||||
|
|
||||||
|
if direction == Outgoing {
|
||||||
|
queue = l.outgoingCommitHooks.newTransients
|
||||||
|
} else {
|
||||||
|
queue = l.incomingCommitHooks.newTransients
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case queue <- hook:
|
||||||
|
case <-l.quit:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isReestablished returns true if the link has successfully completed the
|
// isReestablished returns true if the link has successfully completed the
|
||||||
@@ -1216,6 +1294,33 @@ func (l *channelLink) htlcManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
// We have a new hook that needs to be run when we reach a clean
|
||||||
|
// channel state.
|
||||||
|
case hook := <-l.flushHooks.newTransients:
|
||||||
|
if l.channel.IsChannelClean() {
|
||||||
|
hook()
|
||||||
|
} else {
|
||||||
|
l.flushHooks.alloc(hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a new hook that needs to be run when we have
|
||||||
|
// committed all of our updates.
|
||||||
|
case hook := <-l.outgoingCommitHooks.newTransients:
|
||||||
|
if !l.channel.OweCommitment() {
|
||||||
|
hook()
|
||||||
|
} else {
|
||||||
|
l.outgoingCommitHooks.alloc(hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have a new hook that needs to be run when our peer has
|
||||||
|
// committed all of their updates.
|
||||||
|
case hook := <-l.incomingCommitHooks.newTransients:
|
||||||
|
if !l.channel.NeedCommitment() {
|
||||||
|
hook()
|
||||||
|
} else {
|
||||||
|
l.incomingCommitHooks.alloc(hook)
|
||||||
|
}
|
||||||
|
|
||||||
// Our update fee timer has fired, so we'll check the network
|
// Our update fee timer has fired, so we'll check the network
|
||||||
// fee to see if we should adjust our commitment fee.
|
// fee to see if we should adjust our commitment fee.
|
||||||
case <-l.updateFeeTimer.C:
|
case <-l.updateFeeTimer.C:
|
||||||
@@ -2101,6 +2206,13 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As soon as we are ready to send our next revocation, we can
|
||||||
|
// invoke the incoming commit hooks.
|
||||||
|
l.RWMutex.Lock()
|
||||||
|
l.incomingCommitHooks.invoke()
|
||||||
|
l.RWMutex.Unlock()
|
||||||
|
|
||||||
l.cfg.Peer.SendMessage(false, nextRevocation)
|
l.cfg.Peer.SendMessage(false, nextRevocation)
|
||||||
|
|
||||||
// Notify the incoming htlcs of which the resolutions were
|
// Notify the incoming htlcs of which the resolutions were
|
||||||
@@ -2138,19 +2250,26 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both commitment chains are fully synced from our PoV,
|
// If the remote party initiated the state transition,
|
||||||
// then we don't need to reply with a signature as both sides
|
// we'll reply with a signature to provide them with their
|
||||||
// already have a commitment with the latest accepted.
|
// version of the latest commitment. Otherwise, both commitment
|
||||||
if !l.channel.OweCommitment() {
|
// chains are fully synced from our PoV, then we don't need to
|
||||||
return
|
// reply with a signature as both sides already have a
|
||||||
|
// commitment with the latest accepted.
|
||||||
|
if l.channel.OweCommitment() {
|
||||||
|
if !l.updateCommitTxOrFail() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, the remote party initiated the state transition,
|
// Now that we have finished processing the incoming CommitSig
|
||||||
// so we'll reply with a signature to provide them with their
|
// and sent out our RevokeAndAck, we invoke the flushHooks if
|
||||||
// version of the latest commitment.
|
// the channel state is clean.
|
||||||
if !l.updateCommitTxOrFail() {
|
l.RWMutex.Lock()
|
||||||
return
|
if l.channel.IsChannelClean() {
|
||||||
|
l.flushHooks.invoke()
|
||||||
}
|
}
|
||||||
|
l.RWMutex.Unlock()
|
||||||
|
|
||||||
case *lnwire.RevokeAndAck:
|
case *lnwire.RevokeAndAck:
|
||||||
// We've received a revocation from the remote chain, if valid,
|
// We've received a revocation from the remote chain, if valid,
|
||||||
@@ -2233,6 +2352,14 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now that we have finished processing the RevokeAndAck, we
|
||||||
|
// can invoke the flushHooks if the channel state is clean.
|
||||||
|
l.RWMutex.Lock()
|
||||||
|
if l.channel.IsChannelClean() {
|
||||||
|
l.flushHooks.invoke()
|
||||||
|
}
|
||||||
|
l.RWMutex.Unlock()
|
||||||
|
|
||||||
case *lnwire.UpdateFee:
|
case *lnwire.UpdateFee:
|
||||||
// We received fee update from peer. If we are the initiator we
|
// We received fee update from peer. If we are the initiator we
|
||||||
// will fail the channel, if not we will apply the update.
|
// will fail the channel, if not we will apply the update.
|
||||||
@@ -2441,6 +2568,12 @@ func (l *channelLink) updateCommitTx() error {
|
|||||||
}
|
}
|
||||||
l.cfg.Peer.SendMessage(false, commitSig)
|
l.cfg.Peer.SendMessage(false, commitSig)
|
||||||
|
|
||||||
|
// Now that we have sent out a new CommitSig, we invoke the outgoing set
|
||||||
|
// of commit hooks.
|
||||||
|
l.RWMutex.Lock()
|
||||||
|
l.outgoingCommitHooks.invoke()
|
||||||
|
l.RWMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -7113,7 +7113,7 @@ func TestLinkFlushApiGateStateIdempotence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLinkOutgoingCommitHooksCalled(t *testing.T) {
|
func TestLinkOutgoingCommitHooksCalled(t *testing.T) {
|
||||||
aliceLink, _, batchTicker, start, _, err :=
|
aliceLink, bobChannel, batchTicker, start, _, err :=
|
||||||
newSingleLinkTestHarness(
|
newSingleLinkTestHarness(
|
||||||
t, 5*btcutil.SatoshiPerBitcoin,
|
t, 5*btcutil.SatoshiPerBitcoin,
|
||||||
btcutil.SatoshiPerBitcoin,
|
btcutil.SatoshiPerBitcoin,
|
||||||
@@ -7123,6 +7123,23 @@ func TestLinkOutgoingCommitHooksCalled(t *testing.T) {
|
|||||||
require.NoError(t, start(), "could not start link")
|
require.NoError(t, start(), "could not start link")
|
||||||
|
|
||||||
hookCalled := make(chan struct{})
|
hookCalled := make(chan struct{})
|
||||||
|
|
||||||
|
//nolint:forcetypeassert
|
||||||
|
aliceMsgs := aliceLink.(*channelLink).cfg.Peer.(*mockPeer).sentMsgs
|
||||||
|
|
||||||
|
ctx := linkTestContext{
|
||||||
|
t: t,
|
||||||
|
aliceLink: aliceLink,
|
||||||
|
bobChannel: bobChannel,
|
||||||
|
aliceMsgs: aliceMsgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a pending HTLC on the link.
|
||||||
|
//nolint:forcetypeassert
|
||||||
|
htlc := generateHtlc(t, aliceLink.(*channelLink), 0)
|
||||||
|
ctx.sendHtlcAliceToBob(0, htlc)
|
||||||
|
ctx.receiveHtlcAliceToBob()
|
||||||
|
|
||||||
aliceLink.OnCommitOnce(Outgoing, func() {
|
aliceLink.OnCommitOnce(Outgoing, func() {
|
||||||
close(hookCalled)
|
close(hookCalled)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5466,6 +5466,17 @@ func (lc *LightningChannel) OweCommitment() bool {
|
|||||||
return lc.oweCommitment(true)
|
return lc.oweCommitment(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NeedCommitment returns a boolean value reflecting whether we are waiting on
|
||||||
|
// a commitment signature because there are outstanding remote updates and/or
|
||||||
|
// updates in the remote commit tx that aren't reflected in the local commit tx
|
||||||
|
// yet.
|
||||||
|
func (lc *LightningChannel) NeedCommitment() bool {
|
||||||
|
lc.RLock()
|
||||||
|
defer lc.RUnlock()
|
||||||
|
|
||||||
|
return lc.oweCommitment(false)
|
||||||
|
}
|
||||||
|
|
||||||
// oweCommitment is the internal version of OweCommitment. This function expects
|
// oweCommitment is the internal version of OweCommitment. This function expects
|
||||||
// to be executed with a lock held.
|
// to be executed with a lock held.
|
||||||
func (lc *LightningChannel) oweCommitment(local bool) bool {
|
func (lc *LightningChannel) oweCommitment(local bool) bool {
|
||||||
|
|||||||
+47
-13
@@ -4,9 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -456,7 +458,9 @@ func (m *mockMessageSwitch) GetLinksByInterface(pub [33]byte) (
|
|||||||
// mockUpdateHandler is a mock implementation of the ChannelUpdateHandler
|
// mockUpdateHandler is a mock implementation of the ChannelUpdateHandler
|
||||||
// interface. It is used in mockMessageSwitch's GetLinksByInterface method.
|
// interface. It is used in mockMessageSwitch's GetLinksByInterface method.
|
||||||
type mockUpdateHandler struct {
|
type mockUpdateHandler struct {
|
||||||
cid lnwire.ChannelID
|
cid lnwire.ChannelID
|
||||||
|
isOutgoingAddBlocked atomic.Bool
|
||||||
|
isIncomingAddBlocked atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMockUpdateHandler creates a new mockUpdateHandler.
|
// newMockUpdateHandler creates a new mockUpdateHandler.
|
||||||
@@ -509,25 +513,55 @@ type mockMessageConn struct {
|
|||||||
readRaceDetectingCounter int
|
readRaceDetectingCounter int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockUpdateHandler) EnableAdds(
|
func (m *mockUpdateHandler) EnableAdds(dir htlcswitch.LinkDirection) error {
|
||||||
htlcswitch.LinkDirection,
|
switch dir {
|
||||||
) error {
|
case htlcswitch.Outgoing:
|
||||||
// TODO(proofofkeags): Implement
|
if !m.isOutgoingAddBlocked.Swap(false) {
|
||||||
|
return fmt.Errorf("%v adds already enabled", dir)
|
||||||
|
}
|
||||||
|
case htlcswitch.Incoming:
|
||||||
|
if !m.isIncomingAddBlocked.Swap(false) {
|
||||||
|
return fmt.Errorf("%v adds already enabled", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (m *mockUpdateHandler) DisableAdds(htlcswitch.LinkDirection) error {
|
|
||||||
// TODO(proofofkeags): Implement
|
func (m *mockUpdateHandler) DisableAdds(dir htlcswitch.LinkDirection) error {
|
||||||
|
switch dir {
|
||||||
|
case htlcswitch.Outgoing:
|
||||||
|
if m.isOutgoingAddBlocked.Swap(true) {
|
||||||
|
return fmt.Errorf("%v adds already disabled", dir)
|
||||||
|
}
|
||||||
|
case htlcswitch.Incoming:
|
||||||
|
if m.isIncomingAddBlocked.Swap(true) {
|
||||||
|
return fmt.Errorf("%v adds already disabled", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (m *mockUpdateHandler) IsFlushing(htlcswitch.LinkDirection) bool {
|
|
||||||
// TODO(proofofkeags): Implement
|
func (m *mockUpdateHandler) IsFlushing(dir htlcswitch.LinkDirection) bool {
|
||||||
|
switch dir {
|
||||||
|
case htlcswitch.Outgoing:
|
||||||
|
return m.isOutgoingAddBlocked.Load()
|
||||||
|
case htlcswitch.Incoming:
|
||||||
|
return m.isIncomingAddBlocked.Load()
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func (m *mockUpdateHandler) OnFlushedOnce(func()) {
|
|
||||||
// TODO(proofofkeags): Implement
|
func (m *mockUpdateHandler) OnFlushedOnce(hook func()) {
|
||||||
|
hook()
|
||||||
}
|
}
|
||||||
func (m *mockUpdateHandler) OnCommitOnce(htlcswitch.LinkDirection, func()) {
|
func (m *mockUpdateHandler) OnCommitOnce(
|
||||||
// TODO(proofofkeags): Implement
|
_ htlcswitch.LinkDirection, hook func(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
hook()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockConn(t *testing.T, expectedMessages int) *mockMessageConn {
|
func newMockConn(t *testing.T, expectedMessages int) *mockMessageConn {
|
||||||
|
|||||||
Reference in new issue
Block a user