Merge pull request #6232 from bottlepay/require-interceptor

htlcswitch: add an always on mode to htlc interceptor
This commit is contained in:
Oliver Gugger authored and GitHub committed 2022-03-19 10:47:22 +01:00
commit 57840bba36
13 files changed
+570 -251

No files matched your search

+4
View File
@@ -354,6 +354,10 @@ type Config struct {
RejectHTLC bool `long:"rejecthtlc" description:"If true, lnd will not forward any HTLCs that are meant as onward payments. This option will still allow lnd to send HTLCs and receive HTLCs but lnd won't be used as a hop."` RejectHTLC bool `long:"rejecthtlc" description:"If true, lnd will not forward any HTLCs that are meant as onward payments. This option will still allow lnd to send HTLCs and receive HTLCs but lnd won't be used as a hop."`
// RequireInterceptor determines whether the HTLC interceptor is
// registered regardless of whether the RPC is called or not.
RequireInterceptor bool `long:"requireinterceptor" description:"Whether to always intercept HTLCs, even if no stream is attached"`
StaggerInitialReconnect bool `long:"stagger-initial-reconnect" description:"If true, will apply a randomized staggering between 0s and 30s when reconnecting to persistent peers on startup. The first 10 reconnections will be attempted instantly, regardless of the flag's value"` StaggerInitialReconnect bool `long:"stagger-initial-reconnect" description:"If true, will apply a randomized staggering between 0s and 30s when reconnecting to persistent peers on startup. The first 10 reconnections will be attempted instantly, regardless of the flag's value"`
MaxOutgoingCltvExpiry uint32 `long:"max-cltv-expiry" description:"The maximum number of blocks funds could be locked up for when forwarding payments."` MaxOutgoingCltvExpiry uint32 `long:"max-cltv-expiry" description:"The maximum number of blocks funds could be locked up for when forwarding payments."`
@@ -109,6 +109,10 @@
change, it allows encrypted failure messages to be returned to the sender. change, it allows encrypted failure messages to be returned to the sender.
Additionally it is possible to signal a malformed htlc. Additionally it is possible to signal a malformed htlc.
* Add an [always on](https://github.com/lightningnetwork/lnd/pull/6232) mode to
the HTLC interceptor API. This enables interception applications where every
packet must be intercepted.
## Database ## Database
* [Add ForAll implementation for etcd to speed up * [Add ForAll implementation for etcd to speed up
+304 -42
View File
@@ -30,63 +30,289 @@ var (
// Settle - routes UpdateFulfillHTLC to the originating link. // Settle - routes UpdateFulfillHTLC to the originating link.
// Fail - routes UpdateFailHTLC to the originating link. // Fail - routes UpdateFailHTLC to the originating link.
type InterceptableSwitch struct { type InterceptableSwitch struct {
sync.RWMutex
// htlcSwitch is the underline switch // htlcSwitch is the underline switch
htlcSwitch *Switch htlcSwitch *Switch
// fwdInterceptor is the callback that is called for each forward of // intercepted is where we stream all intercepted packets coming from
// an incoming htlc. It should return true if it is interested in handling // the switch.
// it. intercepted chan *interceptedPackets
fwdInterceptor ForwardInterceptor
// resolutionChan is where we stream all responses coming from the
// interceptor client.
resolutionChan chan *fwdResolution
// interceptorRegistration is a channel that we use to synchronize
// client connect and disconnect.
interceptorRegistration chan ForwardInterceptor
// requireInterceptor indicates whether processing should block if no
// interceptor is connected.
requireInterceptor bool
// interceptor is the handler for intercepted packets.
interceptor ForwardInterceptor
// holdForwards keeps track of outstanding intercepted forwards.
holdForwards map[channeldb.CircuitKey]InterceptedForward
wg sync.WaitGroup
quit chan struct{}
}
type interceptedPackets struct {
packets []*htlcPacket
linkQuit chan struct{}
isReplay bool
}
// FwdAction defines the various resolution types.
type FwdAction int
const (
// FwdActionResume forwards the intercepted packet to the switch.
FwdActionResume FwdAction = iota
// FwdActionSettle settles the intercepted packet with a preimage.
FwdActionSettle
// FwdActionFail fails the intercepted packet back to the sender.
FwdActionFail
)
// FwdResolution defines the action to be taken on an intercepted packet.
type FwdResolution struct {
// Key is the incoming circuit key of the htlc.
Key channeldb.CircuitKey
// Action is the action to take on the intercepted htlc.
Action FwdAction
// Preimage is the preimage that is to be used for settling if Action is
// FwdActionSettle.
Preimage lntypes.Preimage
// FailureMessage is the encrypted failure message that is to be passed
// back to the sender if action is FwdActionFail.
FailureMessage []byte
// FailureCode is the failure code that is to be passed back to the
// sender if action is FwdActionFail.
FailureCode lnwire.FailCode
}
type fwdResolution struct {
resolution *FwdResolution
errChan chan error
} }
// NewInterceptableSwitch returns an instance of InterceptableSwitch. // NewInterceptableSwitch returns an instance of InterceptableSwitch.
func NewInterceptableSwitch(s *Switch) *InterceptableSwitch { func NewInterceptableSwitch(s *Switch,
return &InterceptableSwitch{htlcSwitch: s} requireInterceptor bool) *InterceptableSwitch {
return &InterceptableSwitch{
htlcSwitch: s,
intercepted: make(chan *interceptedPackets),
interceptorRegistration: make(chan ForwardInterceptor),
holdForwards: make(map[channeldb.CircuitKey]InterceptedForward),
resolutionChan: make(chan *fwdResolution),
requireInterceptor: requireInterceptor,
quit: make(chan struct{}),
}
} }
// SetInterceptor sets the ForwardInterceptor to be used. // SetInterceptor sets the ForwardInterceptor to be used. A nil argument
// unregisters the current interceptor.
func (s *InterceptableSwitch) SetInterceptor( func (s *InterceptableSwitch) SetInterceptor(
interceptor ForwardInterceptor) { interceptor ForwardInterceptor) {
s.Lock() // Synchronize setting the handler with the main loop to prevent race
defer s.Unlock() // conditions.
s.fwdInterceptor = interceptor select {
case s.interceptorRegistration <- interceptor:
case <-s.quit:
}
} }
// ForwardPackets attempts to forward the batch of htlcs through the func (s *InterceptableSwitch) Start() error {
// switch, any failed packets will be returned to the provided s.wg.Add(1)
// ChannelLink. The link's quit signal should be provided to allow go func() {
// cancellation of forwarding during link shutdown. defer s.wg.Done()
func (s *InterceptableSwitch) ForwardPackets(linkQuit chan struct{},
packets ...*htlcPacket) error {
var interceptor ForwardInterceptor s.run()
s.Lock() }()
interceptor = s.fwdInterceptor
s.Unlock()
// Optimize for the case we don't have an interceptor. return nil
if interceptor == nil { }
return s.htlcSwitch.ForwardPackets(linkQuit, packets...)
}
var notIntercepted []*htlcPacket func (s *InterceptableSwitch) Stop() error {
for _, p := range packets { close(s.quit)
if !s.interceptForward(p, interceptor, linkQuit) { s.wg.Wait()
notIntercepted = append(notIntercepted, p)
return nil
}
func (s *InterceptableSwitch) run() {
for {
select {
// An interceptor registration or de-registration came in.
case interceptor := <-s.interceptorRegistration:
s.setInterceptor(interceptor)
case packets := <-s.intercepted:
var notIntercepted []*htlcPacket
for _, p := range packets.packets {
if !s.interceptForward(p, packets.isReplay) {
notIntercepted = append(
notIntercepted, p,
)
}
}
err := s.htlcSwitch.ForwardPackets(
packets.linkQuit, notIntercepted...,
)
if err != nil {
log.Errorf("Cannot forward packets: %v", err)
}
case res := <-s.resolutionChan:
res.errChan <- s.resolve(res.resolution)
case <-s.quit:
return
} }
} }
return s.htlcSwitch.ForwardPackets(linkQuit, notIntercepted...) }
func (s *InterceptableSwitch) sendForward(fwd InterceptedForward) {
err := s.interceptor(fwd.Packet())
if err != nil {
// Only log the error. If we couldn't send the packet, we assume
// that the interceptor will reconnect so that we can retry.
log.Debugf("Interceptor cannot handle forward: %v", err)
}
} }
// interceptForward checks if there is any external interceptor interested in func (s *InterceptableSwitch) setInterceptor(interceptor ForwardInterceptor) {
// this packet. Currently only htlc type of UpdateAddHTLC that are forwarded s.interceptor = interceptor
// are being checked for interception. It can be extended in the future given
// the right use case. // Replay all currently held htlcs. When an interceptor is not required,
// there may be none because they've been cleared after the previous
// disconnect.
if interceptor != nil {
log.Debugf("Interceptor connected")
for _, fwd := range s.holdForwards {
s.sendForward(fwd)
}
return
}
// The interceptor disconnects. If an interceptor is required, keep the
// held htlcs.
if s.requireInterceptor {
log.Infof("Interceptor disconnected, retaining held packets")
return
}
// Interceptor is not required. Release held forwards.
log.Infof("Interceptor disconnected, resolving held packets")
for _, fwd := range s.holdForwards {
if err := fwd.Resume(); err != nil {
log.Errorf("Failed to resume hold forward %v", err)
}
}
s.holdForwards = make(map[channeldb.CircuitKey]InterceptedForward)
}
func (s *InterceptableSwitch) resolve(res *FwdResolution) error {
intercepted, ok := s.holdForwards[res.Key]
if !ok {
return fmt.Errorf("fwd %v not found", res.Key)
}
delete(s.holdForwards, res.Key)
switch res.Action {
case FwdActionResume:
return intercepted.Resume()
case FwdActionSettle:
return intercepted.Settle(res.Preimage)
case FwdActionFail:
if len(res.FailureMessage) > 0 {
return intercepted.Fail(res.FailureMessage)
}
return intercepted.FailWithCode(res.FailureCode)
default:
return fmt.Errorf("unrecognized action %v", res.Action)
}
}
// Resolve resolves an intercepted packet.
func (s *InterceptableSwitch) Resolve(res *FwdResolution) error {
internalRes := &fwdResolution{
resolution: res,
errChan: make(chan error, 1),
}
select {
case s.resolutionChan <- internalRes:
case <-s.quit:
return errors.New("switch shutting down")
}
select {
case err := <-internalRes.errChan:
return err
case <-s.quit:
return errors.New("switch shutting down")
}
}
// ForwardPackets attempts to forward the batch of htlcs to a connected
// interceptor. If the interceptor signals the resume action, the htlcs are
// forwarded to the switch. The link's quit signal should be provided to allow
// cancellation of forwarding during link shutdown.
func (s *InterceptableSwitch) ForwardPackets(linkQuit chan struct{}, isReplay bool,
packets ...*htlcPacket) error {
// Synchronize with the main event loop. This should be light in the
// case where there is no interceptor.
select {
case s.intercepted <- &interceptedPackets{
packets: packets,
linkQuit: linkQuit,
isReplay: isReplay,
}:
case <-linkQuit:
log.Debugf("Forward cancelled because link quit")
case <-s.quit:
return errors.New("interceptable switch quit")
}
return nil
}
// interceptForward forwards the packet to the external interceptor after
// checking the interception criteria.
func (s *InterceptableSwitch) interceptForward(packet *htlcPacket, func (s *InterceptableSwitch) interceptForward(packet *htlcPacket,
interceptor ForwardInterceptor, linkQuit chan struct{}) bool { isReplay bool) bool {
// Process normally if an interceptor is not required and not
// registered.
if !s.requireInterceptor && s.interceptor == nil {
return false
}
switch htlc := packet.htlc.(type) { switch htlc := packet.htlc.(type) {
case *lnwire.UpdateAddHTLC: case *lnwire.UpdateAddHTLC:
@@ -95,15 +321,50 @@ func (s *InterceptableSwitch) interceptForward(packet *htlcPacket,
return false return false
} }
inKey := channeldb.CircuitKey{
ChanID: packet.incomingChanID,
HtlcID: packet.incomingHTLCID,
}
// Ignore already held htlcs.
if _, ok := s.holdForwards[inKey]; ok {
return true
}
intercepted := &interceptedForward{ intercepted := &interceptedForward{
linkQuit: linkQuit,
htlc: htlc, htlc: htlc,
packet: packet, packet: packet,
htlcSwitch: s.htlcSwitch, htlcSwitch: s.htlcSwitch,
} }
// If this htlc was intercepted, don't handle the forward. if s.interceptor == nil && !isReplay {
return interceptor(intercepted) // There is no interceptor registered, we are in
// interceptor-required mode, and this is a new packet
//
// Because the interceptor has never seen this packet
// yet, it is still safe to fail back. This limits the
// backlog of htlcs when the interceptor is down.
err := intercepted.FailWithCode(
lnwire.CodeTemporaryChannelFailure,
)
if err != nil {
log.Errorf("Cannot fail packet: %v", err)
}
return true
}
s.holdForwards[inKey] = intercepted
// If there is no interceptor registered, we must be in
// interceptor-required mode. The packet is kept in the queue
// until the interceptor registers itself.
if s.interceptor != nil {
s.sendForward(intercepted)
}
return true
default: default:
return false return false
} }
@@ -113,7 +374,6 @@ func (s *InterceptableSwitch) interceptForward(packet *htlcPacket,
// It is passed from the switch to external interceptors that are interested // It is passed from the switch to external interceptors that are interested
// in holding forwards and resolve them manually. // in holding forwards and resolve them manually.
type interceptedForward struct { type interceptedForward struct {
linkQuit chan struct{}
htlc *lnwire.UpdateAddHTLC htlc *lnwire.UpdateAddHTLC
packet *htlcPacket packet *htlcPacket
htlcSwitch *Switch htlcSwitch *Switch
@@ -139,10 +399,12 @@ func (f *interceptedForward) Packet() InterceptedPacket {
// Resume resumes the default behavior as if the packet was not intercepted. // Resume resumes the default behavior as if the packet was not intercepted.
func (f *interceptedForward) Resume() error { func (f *interceptedForward) Resume() error {
return f.htlcSwitch.ForwardPackets(f.linkQuit, f.packet) // Forward to the switch. A link quit channel isn't needed, because we
// are on a different thread now.
return f.htlcSwitch.ForwardPackets(nil, f.packet)
} }
// Fail notifies the intention to fail an existing hold forward with an // Fail notifies the intention to Fail an existing hold forward with an
// encrypted failure reason. // encrypted failure reason.
func (f *interceptedForward) Fail(reason []byte) error { func (f *interceptedForward) Fail(reason []byte) error {
obfuscatedReason := f.packet.obfuscator.IntermediateEncrypt(reason) obfuscatedReason := f.packet.obfuscator.IntermediateEncrypt(reason)
+4 -1
View File
@@ -234,6 +234,9 @@ type TowerClient interface {
type InterceptableHtlcForwarder interface { type InterceptableHtlcForwarder interface {
// SetInterceptor sets a ForwardInterceptor. // SetInterceptor sets a ForwardInterceptor.
SetInterceptor(interceptor ForwardInterceptor) SetInterceptor(interceptor ForwardInterceptor)
// Resolve resolves an intercepted packet.
Resolve(res *FwdResolution) error
} }
// ForwardInterceptor is a function that is invoked from the switch for every // ForwardInterceptor is a function that is invoked from the switch for every
@@ -242,7 +245,7 @@ type InterceptableHtlcForwarder interface {
// to resolve it manually later in case it is held. // to resolve it manually later in case it is held.
// The return value indicates if this handler will take control of this forward // The return value indicates if this handler will take control of this forward
// and resolve it later or let the switch execute its default behavior. // and resolve it later or let the switch execute its default behavior.
type ForwardInterceptor func(InterceptedForward) bool type ForwardInterceptor func(InterceptedPacket) error
// InterceptedPacket contains the relevant information for the interceptor about // InterceptedPacket contains the relevant information for the interceptor about
// an htlc. // an htlc.
+11 -7
View File
@@ -141,7 +141,7 @@ type ChannelLinkConfig struct {
// switch. The function returns and error in case it fails to send one or // switch. The function returns and error in case it fails to send one or
// more packets. The link's quit signal should be provided to allow // more packets. The link's quit signal should be provided to allow
// cancellation of forwarding during link shutdown. // cancellation of forwarding during link shutdown.
ForwardPackets func(chan struct{}, ...*htlcPacket) error ForwardPackets func(chan struct{}, bool, ...*htlcPacket) error
// DecodeHopIterators facilitates batched decoding of HTLC Sphinx onion // DecodeHopIterators facilitates batched decoding of HTLC Sphinx onion
// blobs, which are then used to inform how to forward an HTLC. // blobs, which are then used to inform how to forward an HTLC.
@@ -1720,7 +1720,7 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
l.uncommittedPreimages = append(l.uncommittedPreimages, pre) l.uncommittedPreimages = append(l.uncommittedPreimages, pre)
// Pipeline this settle, send it to the switch. // Pipeline this settle, send it to the switch.
go l.forwardBatch(settlePacket) go l.forwardBatch(false, settlePacket)
case *lnwire.UpdateFailMalformedHTLC: case *lnwire.UpdateFailMalformedHTLC:
// Convert the failure type encoded within the HTLC fail // Convert the failure type encoded within the HTLC fail
@@ -2744,7 +2744,7 @@ func (l *channelLink) processRemoteSettleFails(fwdPkg *channeldb.FwdPkg,
// Only spawn the task forward packets we have a non-zero number. // Only spawn the task forward packets we have a non-zero number.
if len(switchPackets) > 0 { if len(switchPackets) > 0 {
go l.forwardBatch(switchPackets...) go l.forwardBatch(false, switchPackets...)
} }
} }
@@ -3043,14 +3043,17 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
return return
} }
l.log.Debugf("forwarding %d packets to switch", len(switchPackets)) replay := fwdPkg.State != channeldb.FwdStateLockedIn
l.log.Debugf("forwarding %d packets to switch: replay=%v",
len(switchPackets), replay)
// NOTE: This call is made synchronous so that we ensure all circuits // NOTE: This call is made synchronous so that we ensure all circuits
// are committed in the exact order that they are processed in the link. // are committed in the exact order that they are processed in the link.
// Failing to do this could cause reorderings/gaps in the range of // Failing to do this could cause reorderings/gaps in the range of
// opened circuits, which violates assumptions made by the circuit // opened circuits, which violates assumptions made by the circuit
// trimming. // trimming.
l.forwardBatch(switchPackets...) l.forwardBatch(replay, switchPackets...)
} }
// processExitHop handles an htlc for which this link is the exit hop. It // processExitHop handles an htlc for which this link is the exit hop. It
@@ -3184,7 +3187,7 @@ func (l *channelLink) settleHTLC(preimage lntypes.Preimage,
// forwardBatch forwards the given htlcPackets to the switch, and waits on the // forwardBatch forwards the given htlcPackets to the switch, and waits on the
// err chan for the individual responses. This method is intended to be spawned // err chan for the individual responses. This method is intended to be spawned
// as a goroutine so the responses can be handled in the background. // as a goroutine so the responses can be handled in the background.
func (l *channelLink) forwardBatch(packets ...*htlcPacket) { func (l *channelLink) forwardBatch(replay bool, packets ...*htlcPacket) {
// Don't forward packets for which we already have a response in our // Don't forward packets for which we already have a response in our
// mailbox. This could happen if a packet fails and is buffered in the // mailbox. This could happen if a packet fails and is buffered in the
// mailbox, and the incoming link flaps. // mailbox, and the incoming link flaps.
@@ -3197,7 +3200,8 @@ func (l *channelLink) forwardBatch(packets ...*htlcPacket) {
filteredPkts = append(filteredPkts, pkt) filteredPkts = append(filteredPkts, pkt)
} }
if err := l.cfg.ForwardPackets(l.quit, filteredPkts...); err != nil { err := l.cfg.ForwardPackets(l.quit, replay, filteredPkts...)
if err != nil {
log.Errorf("Unhandled error while reforwarding htlc "+ log.Errorf("Unhandled error while reforwarding htlc "+
"settle/fail over htlcswitch: %v", err) "settle/fail over htlcswitch: %v", err)
} }
+17 -13
View File
@@ -1940,12 +1940,14 @@ func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) (
// the firing via force feeding. // the firing via force feeding.
bticker := ticker.NewForce(time.Hour) bticker := ticker.NewForce(time.Hour)
aliceCfg := ChannelLinkConfig{ aliceCfg := ChannelLinkConfig{
FwrdingPolicy: globalPolicy, FwrdingPolicy: globalPolicy,
Peer: alicePeer, Peer: alicePeer,
Switch: aliceSwitch, Switch: aliceSwitch,
BestHeight: aliceSwitch.BestHeight, BestHeight: aliceSwitch.BestHeight,
Circuits: aliceSwitch.CircuitModifier(), Circuits: aliceSwitch.CircuitModifier(),
ForwardPackets: aliceSwitch.ForwardPackets, ForwardPackets: func(linkQuit chan struct{}, _ bool, packets ...*htlcPacket) error {
return aliceSwitch.ForwardPackets(linkQuit, packets...)
},
DecodeHopIterators: decoder.DecodeHopIterators, DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) ( ExtractErrorEncrypter: func(*btcec.PublicKey) (
hop.ErrorEncrypter, lnwire.FailCode) { hop.ErrorEncrypter, lnwire.FailCode) {
@@ -4491,12 +4493,14 @@ func (h *persistentLinkHarness) restartLink(
// the firing via force feeding. // the firing via force feeding.
bticker := ticker.NewForce(time.Hour) bticker := ticker.NewForce(time.Hour)
aliceCfg := ChannelLinkConfig{ aliceCfg := ChannelLinkConfig{
FwrdingPolicy: globalPolicy, FwrdingPolicy: globalPolicy,
Peer: alicePeer, Peer: alicePeer,
Switch: aliceSwitch, Switch: aliceSwitch,
BestHeight: aliceSwitch.BestHeight, BestHeight: aliceSwitch.BestHeight,
Circuits: aliceSwitch.CircuitModifier(), Circuits: aliceSwitch.CircuitModifier(),
ForwardPackets: aliceSwitch.ForwardPackets, ForwardPackets: func(linkQuit chan struct{}, _ bool, packets ...*htlcPacket) error {
return aliceSwitch.ForwardPackets(linkQuit, packets...)
},
DecodeHopIterators: decoder.DecodeHopIterators, DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) ( ExtractErrorEncrypter: func(*btcec.PublicKey) (
hop.ErrorEncrypter, lnwire.FailCode) { hop.ErrorEncrypter, lnwire.FailCode) {
@@ -6694,7 +6698,7 @@ func TestPipelineSettle(t *testing.T) {
// erroneously forwarded. If the forwardChan is closed before the last // erroneously forwarded. If the forwardChan is closed before the last
// step, then the test will fail. // step, then the test will fail.
forwardChan := make(chan struct{}) forwardChan := make(chan struct{})
fwdPkts := func(c chan struct{}, hp ...*htlcPacket) error { fwdPkts := func(c chan struct{}, _ bool, hp ...*htlcPacket) error {
close(forwardChan) close(forwardChan)
return nil return nil
} }
+161 -46
View File
@@ -3140,32 +3140,29 @@ func getThreeHopEvents(channels *clusterChannels, htlcID uint64,
} }
type mockForwardInterceptor struct { type mockForwardInterceptor struct {
intercepted InterceptedForward t *testing.T
interceptedChan chan InterceptedPacket
} }
func (m *mockForwardInterceptor) InterceptForwardHtlc( func (m *mockForwardInterceptor) InterceptForwardHtlc(
intercepted InterceptedForward) bool { intercepted InterceptedPacket) error {
m.intercepted = intercepted m.interceptedChan <- intercepted
return true
return nil
} }
func (m *mockForwardInterceptor) settle(preimage lntypes.Preimage) error { func (m *mockForwardInterceptor) getIntercepted() InterceptedPacket {
return m.intercepted.Settle(preimage) select {
} case p := <-m.interceptedChan:
return p
func (m *mockForwardInterceptor) fail(reason []byte) error { case <-time.After(time.Second):
return m.intercepted.Fail(reason) require.Fail(m.t, "timeout")
}
func (m *mockForwardInterceptor) failWithCode( return InterceptedPacket{}
code lnwire.FailCode) error { }
return m.intercepted.FailWithCode(code)
}
func (m *mockForwardInterceptor) resume() error {
return m.intercepted.Resume()
} }
func assertNumCircuits(t *testing.T, s *Switch, pending, opened int) { func assertNumCircuits(t *testing.T, s *Switch, pending, opened int) {
@@ -3272,22 +3269,28 @@ func TestSwitchHoldForward(t *testing.T) {
}, },
} }
forwardInterceptor := &mockForwardInterceptor{} forwardInterceptor := &mockForwardInterceptor{
switchForwardInterceptor := NewInterceptableSwitch(s) t: t,
interceptedChan: make(chan InterceptedPacket),
}
switchForwardInterceptor := NewInterceptableSwitch(s, false)
require.NoError(t, switchForwardInterceptor.Start())
switchForwardInterceptor.SetInterceptor(forwardInterceptor.InterceptForwardHtlc) switchForwardInterceptor.SetInterceptor(forwardInterceptor.InterceptForwardHtlc)
linkQuit := make(chan struct{}) linkQuit := make(chan struct{})
// Test resume a hold forward // Test resume a hold forward.
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
if err := switchForwardInterceptor.ForwardPackets(linkQuit, ogPacket); err != nil { err = switchForwardInterceptor.ForwardPackets(linkQuit, false, ogPacket)
t.Fatalf("can't forward htlc packet: %v", err) require.NoError(t, err)
}
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
if err := forwardInterceptor.resume(); err != nil { require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
t.Fatalf("failed to resume forward") Action: FwdActionResume,
} Key: forwardInterceptor.getIntercepted().IncomingCircuit,
}))
assertOutgoingLinkReceive(t, bobChannelLink, true) assertOutgoingLinkReceive(t, bobChannelLink, true)
assertNumCircuits(t, s, 1, 1) assertNumCircuits(t, s, 1, 1)
@@ -3300,35 +3303,72 @@ func TestSwitchHoldForward(t *testing.T) {
PaymentPreimage: preimage, PaymentPreimage: preimage,
}, },
} }
if err := switchForwardInterceptor.ForwardPackets(linkQuit, settle); err != nil { err = switchForwardInterceptor.ForwardPackets(linkQuit, false, settle)
t.Fatalf("can't forward htlc packet: %v", err) require.NoError(t, err)
}
assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0)
// Test resume a hold forward after disconnection.
require.NoError(t, switchForwardInterceptor.ForwardPackets(
linkQuit, false, ogPacket,
))
// Wait until the packet is offered to the interceptor.
_ = forwardInterceptor.getIntercepted()
// No forward expected yet.
assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false)
// Disconnect should resume the forwarding.
switchForwardInterceptor.SetInterceptor(nil)
assertOutgoingLinkReceive(t, bobChannelLink, true)
assertNumCircuits(t, s, 1, 1)
// Settle the htlc to close the circuit.
settle.outgoingHTLCID = 1
require.NoError(t, switchForwardInterceptor.ForwardPackets(
linkQuit, false, settle,
))
assertOutgoingLinkReceive(t, aliceChannelLink, true) assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
// Test failing a hold forward // Test failing a hold forward
if err := switchForwardInterceptor.ForwardPackets(linkQuit, ogPacket); err != nil { switchForwardInterceptor.SetInterceptor(
t.Fatalf("can't forward htlc packet: %v", err) forwardInterceptor.InterceptForwardHtlc,
} )
require.NoError(t, switchForwardInterceptor.ForwardPackets(
linkQuit, false, ogPacket,
))
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
if err := forwardInterceptor.fail(nil); err != nil { require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
t.Fatalf("failed to cancel forward %v", err) Action: FwdActionFail,
} Key: forwardInterceptor.getIntercepted().IncomingCircuit,
FailureCode: lnwire.CodeTemporaryChannelFailure,
}))
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
assertOutgoingLinkReceive(t, aliceChannelLink, true) assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
// Test failing a hold forward with a failure message. // Test failing a hold forward with a failure message.
require.NoError(t, require.NoError(t,
switchForwardInterceptor.ForwardPackets(linkQuit, ogPacket), switchForwardInterceptor.ForwardPackets(linkQuit, false, ogPacket),
) )
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
reason := lnwire.OpaqueReason([]byte{1, 2, 3}) reason := lnwire.OpaqueReason([]byte{1, 2, 3})
require.NoError(t, forwardInterceptor.fail(reason)) require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
Action: FwdActionFail,
Key: forwardInterceptor.getIntercepted().IncomingCircuit,
FailureMessage: reason,
}))
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
packet := assertOutgoingLinkReceive(t, aliceChannelLink, true) packet := assertOutgoingLinkReceive(t, aliceChannelLink, true)
@@ -3338,14 +3378,18 @@ func TestSwitchHoldForward(t *testing.T) {
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
// Test failing a hold forward with a malformed htlc failure. // Test failing a hold forward with a malformed htlc failure.
err = switchForwardInterceptor.ForwardPackets(linkQuit, ogPacket) err = switchForwardInterceptor.ForwardPackets(linkQuit, false, ogPacket)
require.NoError(t, err) require.NoError(t, err)
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
code := lnwire.CodeInvalidOnionKey code := lnwire.CodeInvalidOnionKey
require.NoError(t, forwardInterceptor.failWithCode(code)) require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
Action: FwdActionFail,
Key: forwardInterceptor.getIntercepted().IncomingCircuit,
FailureCode: code,
}))
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
packet = assertOutgoingLinkReceive(t, aliceChannelLink, true) packet = assertOutgoingLinkReceive(t, aliceChannelLink, true)
@@ -3363,18 +3407,89 @@ func TestSwitchHoldForward(t *testing.T) {
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
// Test settling a hold forward // Test settling a hold forward
if err := switchForwardInterceptor.ForwardPackets(linkQuit, ogPacket); err != nil { require.NoError(t, switchForwardInterceptor.ForwardPackets(
t.Fatalf("can't forward htlc packet: %v", err) linkQuit, false, ogPacket,
} ))
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
if err := forwardInterceptor.settle(preimage); err != nil { require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
t.Fatal("failed to cancel forward") Key: forwardInterceptor.getIntercepted().IncomingCircuit,
} Action: FwdActionSettle,
Preimage: preimage,
}))
assertOutgoingLinkReceive(t, bobChannelLink, false) assertOutgoingLinkReceive(t, bobChannelLink, false)
assertOutgoingLinkReceive(t, aliceChannelLink, true) assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0) assertNumCircuits(t, s, 0, 0)
require.NoError(t, switchForwardInterceptor.Stop())
// Test always-on interception.
switchForwardInterceptor = NewInterceptableSwitch(s, true)
require.NoError(t, switchForwardInterceptor.Start())
// Forward a fresh packet. It is expected to be failed immediately,
// because there is no interceptor registered.
require.NoError(t, switchForwardInterceptor.ForwardPackets(
linkQuit, false, ogPacket,
))
assertOutgoingLinkReceive(t, bobChannelLink, false)
assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0)
// Forward a replayed packet. It is expected to be held until the
// interceptor connects. To continue the test, it needs to be ran in a
// goroutine.
errChan := make(chan error)
go func() {
errChan <- switchForwardInterceptor.ForwardPackets(
linkQuit, true, ogPacket,
)
}()
// Assert that nothing is forward to the switch.
assertOutgoingLinkReceive(t, bobChannelLink, false)
assertNumCircuits(t, s, 0, 0)
// Register an interceptor.
switchForwardInterceptor.SetInterceptor(
forwardInterceptor.InterceptForwardHtlc,
)
// Expect the ForwardPackets call to unblock.
require.NoError(t, <-errChan)
// Now expect the queued packet to come through.
forwardInterceptor.getIntercepted()
// Disconnect and reconnect interceptor.
switchForwardInterceptor.SetInterceptor(nil)
switchForwardInterceptor.SetInterceptor(
forwardInterceptor.InterceptForwardHtlc,
)
// A replay of the held packet is expected.
intercepted := forwardInterceptor.getIntercepted()
// Settle the packet.
require.NoError(t, switchForwardInterceptor.resolve(&FwdResolution{
Key: intercepted.IncomingCircuit,
Action: FwdActionSettle,
Preimage: preimage,
}))
assertOutgoingLinkReceive(t, bobChannelLink, false)
assertOutgoingLinkReceive(t, aliceChannelLink, true)
assertNumCircuits(t, s, 0, 0)
require.NoError(t, switchForwardInterceptor.Stop())
select {
case <-forwardInterceptor.interceptedChan:
require.Fail(t, "unexpected interception")
default:
}
} }
// TestSwitchDustForwarding tests that the switch properly fails HTLC's which // TestSwitchDustForwarding tests that the switch properly fails HTLC's which
+8 -6
View File
@@ -1135,12 +1135,14 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
link := NewChannelLink( link := NewChannelLink(
ChannelLinkConfig{ ChannelLinkConfig{
Switch: server.htlcSwitch, Switch: server.htlcSwitch,
BestHeight: server.htlcSwitch.BestHeight, BestHeight: server.htlcSwitch.BestHeight,
FwrdingPolicy: h.globalPolicy, FwrdingPolicy: h.globalPolicy,
Peer: peer, Peer: peer,
Circuits: server.htlcSwitch.CircuitModifier(), Circuits: server.htlcSwitch.CircuitModifier(),
ForwardPackets: server.htlcSwitch.ForwardPackets, ForwardPackets: func(linkQuit chan struct{}, _ bool, packets ...*htlcPacket) error {
return server.htlcSwitch.ForwardPackets(linkQuit, packets...)
},
DecodeHopIterators: decoder.DecodeHopIterators, DecodeHopIterators: decoder.DecodeHopIterators,
ExtractErrorEncrypter: func(*btcec.PublicKey) ( ExtractErrorEncrypter: func(*btcec.PublicKey) (
hop.ErrorEncrypter, lnwire.FailCode) { hop.ErrorEncrypter, lnwire.FailCode) {
+39 -133
View File
@@ -2,7 +2,6 @@ package routerrpc
import ( import (
"errors" "errors"
"sync"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
@@ -27,36 +26,19 @@ var (
// interceptor streaming session. // interceptor streaming session.
// It is created when the stream opens and disconnects when the stream closes. // It is created when the stream opens and disconnects when the stream closes.
type forwardInterceptor struct { type forwardInterceptor struct {
// server is the Server reference
server *Server
// holdForwards is a map of current hold forwards and their corresponding
// ForwardResolver.
holdForwards map[channeldb.CircuitKey]htlcswitch.InterceptedForward
// stream is the bidirectional RPC stream // stream is the bidirectional RPC stream
stream Router_HtlcInterceptorServer stream Router_HtlcInterceptorServer
// quit is a channel that is closed when this forwardInterceptor is shutting htlcSwitch htlcswitch.InterceptableHtlcForwarder
// down.
quit chan struct{}
// intercepted is where we stream all intercepted packets coming from
// the switch.
intercepted chan htlcswitch.InterceptedForward
wg sync.WaitGroup
} }
// newForwardInterceptor creates a new forwardInterceptor. // newForwardInterceptor creates a new forwardInterceptor.
func newForwardInterceptor(server *Server, stream Router_HtlcInterceptorServer) *forwardInterceptor { func newForwardInterceptor(htlcSwitch htlcswitch.InterceptableHtlcForwarder,
stream Router_HtlcInterceptorServer) *forwardInterceptor {
return &forwardInterceptor{ return &forwardInterceptor{
server: server, htlcSwitch: htlcSwitch,
stream: stream, stream: stream,
holdForwards: make(
map[channeldb.CircuitKey]htlcswitch.InterceptedForward),
quit: make(chan struct{}),
intercepted: make(chan htlcswitch.InterceptedForward),
} }
} }
@@ -67,42 +49,18 @@ func newForwardInterceptor(server *Server, stream Router_HtlcInterceptorServer)
// To coordinate all this and make sure it is safe for concurrent access all // To coordinate all this and make sure it is safe for concurrent access all
// packets are sent to the main where they are handled. // packets are sent to the main where they are handled.
func (r *forwardInterceptor) run() error { func (r *forwardInterceptor) run() error {
// make sure we disconnect and resolves all remaining packets if any.
defer r.onDisconnect()
// Register our interceptor so we receive all forwarded packets. // Register our interceptor so we receive all forwarded packets.
interceptableForwarder := r.server.cfg.RouterBackend.InterceptableForwarder r.htlcSwitch.SetInterceptor(r.onIntercept)
interceptableForwarder.SetInterceptor(r.onIntercept) defer r.htlcSwitch.SetInterceptor(nil)
defer interceptableForwarder.SetInterceptor(nil)
// start a go routine that reads client resolutions.
errChan := make(chan error)
resolutionRequests := make(chan *ForwardHtlcInterceptResponse)
r.wg.Add(1)
go r.readClientResponses(resolutionRequests, errChan)
// run the main loop that synchronizes both sides input into one go routine.
for { for {
select { resp, err := r.stream.Recv()
case intercepted := <-r.intercepted: if err != nil {
log.Tracef("sending intercepted packet to client %v", intercepted) return err
// in case we couldn't forward we exit the loop and drain the }
// current interceptor as this indicates on a connection problem.
if err := r.holdAndForwardToClient(intercepted); err != nil { if err := r.resolveFromClient(resp); err != nil {
return err
}
case resolution := <-resolutionRequests:
log.Tracef("resolving intercepted packet %v", resolution)
// in case we couldn't resolve we just add a log line since this
// does not indicate on any connection problem.
if err := r.resolveFromClient(resolution); err != nil {
log.Warnf("client resolution of intercepted "+
"packet failed %v", err)
}
case err := <-errChan:
return err return err
case <-r.server.quit:
return nil
} }
} }
} }
@@ -111,54 +69,14 @@ func (r *forwardInterceptor) run() error {
// packet. Our interceptor makes sure we hold the packet and then signal to the // packet. Our interceptor makes sure we hold the packet and then signal to the
// main loop to handle the packet. We only return true if we were able // main loop to handle the packet. We only return true if we were able
// to deliver the packet to the main loop. // to deliver the packet to the main loop.
func (r *forwardInterceptor) onIntercept(p htlcswitch.InterceptedForward) bool { func (r *forwardInterceptor) onIntercept(
select { htlc htlcswitch.InterceptedPacket) error {
case r.intercepted <- p:
return true
case <-r.quit:
return false
case <-r.server.quit:
return false
}
}
func (r *forwardInterceptor) readClientResponses( log.Tracef("Sending intercepted packet to client %v", htlc)
resolutionChan chan *ForwardHtlcInterceptResponse, errChan chan error) {
defer r.wg.Done()
for {
resp, err := r.stream.Recv()
if err != nil {
errChan <- err
return
}
// Now that we have the response from the RPC client, send it to
// the responses chan.
select {
case resolutionChan <- resp:
case <-r.quit:
return
case <-r.server.quit:
return
}
}
}
// holdAndForwardToClient forwards the intercepted htlc to the client.
func (r *forwardInterceptor) holdAndForwardToClient(
forward htlcswitch.InterceptedForward) error {
htlc := forward.Packet()
inKey := htlc.IncomingCircuit inKey := htlc.IncomingCircuit
// Ignore already held htlcs.
if _, ok := r.holdForwards[inKey]; ok {
return nil
}
// First hold the forward, then send to client. // First hold the forward, then send to client.
r.holdForwards[inKey] = forward
interceptionRequest := &ForwardHtlcInterceptRequest{ interceptionRequest := &ForwardHtlcInterceptRequest{
IncomingCircuitKey: &CircuitKey{ IncomingCircuitKey: &CircuitKey{
ChanId: inKey.ChanID.ToUint64(), ChanId: inKey.ChanID.ToUint64(),
@@ -181,20 +99,19 @@ func (r *forwardInterceptor) holdAndForwardToClient(
func (r *forwardInterceptor) resolveFromClient( func (r *forwardInterceptor) resolveFromClient(
in *ForwardHtlcInterceptResponse) error { in *ForwardHtlcInterceptResponse) error {
log.Tracef("Resolving intercepted packet %v", in)
circuitKey := channeldb.CircuitKey{ circuitKey := channeldb.CircuitKey{
ChanID: lnwire.NewShortChanIDFromInt(in.IncomingCircuitKey.ChanId), ChanID: lnwire.NewShortChanIDFromInt(in.IncomingCircuitKey.ChanId),
HtlcID: in.IncomingCircuitKey.HtlcId, HtlcID: in.IncomingCircuitKey.HtlcId,
} }
var interceptedForward htlcswitch.InterceptedForward
interceptedForward, ok := r.holdForwards[circuitKey]
if !ok {
return ErrFwdNotExists
}
delete(r.holdForwards, circuitKey)
switch in.Action { switch in.Action {
case ResolveHoldForwardAction_RESUME: case ResolveHoldForwardAction_RESUME:
return interceptedForward.Resume() return r.htlcSwitch.Resolve(&htlcswitch.FwdResolution{
Key: circuitKey,
Action: htlcswitch.FwdActionResume,
})
case ResolveHoldForwardAction_FAIL: case ResolveHoldForwardAction_FAIL:
// Fail with an encrypted reason. // Fail with an encrypted reason.
@@ -219,7 +136,11 @@ func (r *forwardInterceptor) resolveFromClient(
) )
} }
return interceptedForward.Fail(in.FailureMessage) return r.htlcSwitch.Resolve(&htlcswitch.FwdResolution{
Key: circuitKey,
Action: htlcswitch.FwdActionFail,
FailureMessage: in.FailureMessage,
})
} }
var code lnwire.FailCode var code lnwire.FailCode
@@ -244,14 +165,11 @@ func (r *forwardInterceptor) resolveFromClient(
) )
} }
err := interceptedForward.FailWithCode(code) return r.htlcSwitch.Resolve(&htlcswitch.FwdResolution{
if err == htlcswitch.ErrUnsupportedFailureCode { Key: circuitKey,
return status.Errorf( Action: htlcswitch.FwdActionFail,
codes.InvalidArgument, err.Error(), FailureCode: code,
) })
}
return err
case ResolveHoldForwardAction_SETTLE: case ResolveHoldForwardAction_SETTLE:
if in.Preimage == nil { if in.Preimage == nil {
@@ -261,7 +179,12 @@ func (r *forwardInterceptor) resolveFromClient(
if err != nil { if err != nil {
return err return err
} }
return interceptedForward.Settle(preimage)
return r.htlcSwitch.Resolve(&htlcswitch.FwdResolution{
Key: circuitKey,
Action: htlcswitch.FwdActionSettle,
Preimage: preimage,
})
default: default:
return status.Errorf( return status.Errorf(
@@ -270,20 +193,3 @@ func (r *forwardInterceptor) resolveFromClient(
) )
} }
} }
// onDisconnect removes all previousely held forwards from
// the store. Before they are removed it ensure to resume as the default
// behavior.
func (r *forwardInterceptor) onDisconnect() {
// Then close the channel so all go routine will exit.
close(r.quit)
log.Infof("RPC interceptor disconnected, resolving held packets")
for key, forward := range r.holdForwards {
if err := forward.Resume(); err != nil {
log.Errorf("failed to resume hold forward %v", err)
}
delete(r.holdForwards, key)
}
r.wg.Wait()
}
+3 -1
View File
@@ -890,7 +890,9 @@ func (s *Server) HtlcInterceptor(stream Router_HtlcInterceptorServer) error {
defer atomic.CompareAndSwapInt32(&s.forwardInterceptorActive, 1, 0) defer atomic.CompareAndSwapInt32(&s.forwardInterceptorActive, 1, 0)
// run the forward interceptor. // run the forward interceptor.
return newForwardInterceptor(s, stream).run() return newForwardInterceptor(
s.cfg.RouterBackend.InterceptableForwarder, stream,
).run()
} }
func extractOutPoint(req *UpdateChanStatusRequest) (*wire.OutPoint, error) { func extractOutPoint(req *UpdateChanStatusRequest) (*wire.OutPoint, error) {
+3 -1
View File
@@ -367,7 +367,9 @@ func createTestPeer(notifier chainntnfs.ChainNotifier,
Switch: mockSwitch, Switch: mockSwitch,
ChanActiveTimeout: chanActiveTimeout, ChanActiveTimeout: chanActiveTimeout,
InterceptSwitch: htlcswitch.NewInterceptableSwitch(nil), InterceptSwitch: htlcswitch.NewInterceptableSwitch(
nil, false,
),
ChannelDB: dbAlice.ChannelStateDB(), ChannelDB: dbAlice.ChannelStateDB(),
FeeEstimator: estimator, FeeEstimator: estimator,
+3
View File
@@ -347,6 +347,9 @@
; used as a hop. ; used as a hop.
; rejecthtlc=true ; rejecthtlc=true
; If true, all HTLCs will be held until they are handled by an interceptor
; requireinterceptor=true
; If true, will apply a randomized staggering between 0s and 30s when ; If true, will apply a randomized staggering between 0s and 30s when
; reconnecting to persistent peers on startup. The first 10 reconnections will be ; reconnecting to persistent peers on startup. The first 10 reconnections will be
; attempted instantly, regardless of the flag's value ; attempted instantly, regardless of the flag's value
+9 -1
View File
@@ -654,7 +654,9 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
if err != nil { if err != nil {
return nil, err return nil, err
} }
s.interceptableSwitch = htlcswitch.NewInterceptableSwitch(s.htlcSwitch) s.interceptableSwitch = htlcswitch.NewInterceptableSwitch(
s.htlcSwitch, s.cfg.RequireInterceptor,
)
chanStatusMgrCfg := &netann.ChanStatusConfig{ chanStatusMgrCfg := &netann.ChanStatusConfig{
ChanStatusSampleInterval: cfg.ChanStatusSampleInterval, ChanStatusSampleInterval: cfg.ChanStatusSampleInterval,
@@ -1786,6 +1788,12 @@ func (s *server) Start() error {
} }
cleanup = cleanup.add(s.htlcSwitch.Stop) cleanup = cleanup.add(s.htlcSwitch.Stop)
if err := s.interceptableSwitch.Start(); err != nil {
startErr = err
return
}
cleanup = cleanup.add(s.interceptableSwitch.Stop)
if err := s.chainArb.Start(); err != nil { if err := s.chainArb.Start(); err != nil {
startErr = err startErr = err
return return