diff --git a/chanacceptor/chainedacceptor.go b/chanacceptor/chainedacceptor.go index 55d8feb38..6e160ef4a 100644 --- a/chanacceptor/chainedacceptor.go +++ b/chanacceptor/chainedacceptor.go @@ -23,6 +23,8 @@ func NewChainedAcceptor() *ChainedAcceptor { } // AddAcceptor adds a ChannelAcceptor to this ChainedAcceptor. +// +// NOTE: Part of the MultiplexAcceptor interface. func (c *ChainedAcceptor) AddAcceptor(acceptor ChannelAcceptor) uint64 { id := atomic.AddUint64(&c.acceptorID, 1) @@ -36,12 +38,22 @@ func (c *ChainedAcceptor) AddAcceptor(acceptor ChannelAcceptor) uint64 { // RemoveAcceptor removes a ChannelAcceptor from this ChainedAcceptor given // an ID. +// +// NOTE: Part of the MultiplexAcceptor interface. func (c *ChainedAcceptor) RemoveAcceptor(id uint64) { c.acceptorsMtx.Lock() delete(c.acceptors, id) c.acceptorsMtx.Unlock() } +// numAcceptors returns the number of acceptors contained in the +// ChainedAcceptor. +func (c *ChainedAcceptor) numAcceptors() int { + c.acceptorsMtx.RLock() + defer c.acceptorsMtx.RUnlock() + return len(c.acceptors) +} + // Accept evaluates the results of all ChannelAcceptors in the acceptors map // and returns the conjunction of all these predicates. // @@ -91,5 +103,5 @@ func (c *ChainedAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptRespon } // A compile-time constraint to ensure ChainedAcceptor implements the -// ChannelAcceptor interface. -var _ ChannelAcceptor = (*ChainedAcceptor)(nil) +// MultiplexAcceptor interface. +var _ MultiplexAcceptor = (*ChainedAcceptor)(nil) diff --git a/chanacceptor/interface.go b/chanacceptor/interface.go index 75e7e7e7c..3149cb05b 100644 --- a/chanacceptor/interface.go +++ b/chanacceptor/interface.go @@ -118,3 +118,16 @@ func (c *ChannelAcceptResponse) RejectChannel() bool { type ChannelAcceptor interface { Accept(req *ChannelAcceptRequest) *ChannelAcceptResponse } + +// MultiplexAcceptor is an interface that abstracts the ability of a +// ChannelAcceptor to contain sub-ChannelAcceptors. +type MultiplexAcceptor interface { + // Embed the ChannelAcceptor. + ChannelAcceptor + + // AddAcceptor nests a ChannelAcceptor inside the MultiplexAcceptor. + AddAcceptor(acceptor ChannelAcceptor) uint64 + + // Remove a sub-ChannelAcceptor. + RemoveAcceptor(id uint64) +} diff --git a/chanacceptor/zeroconfacceptor.go b/chanacceptor/zeroconfacceptor.go new file mode 100644 index 000000000..524a4b7ea --- /dev/null +++ b/chanacceptor/zeroconfacceptor.go @@ -0,0 +1,68 @@ +package chanacceptor + +import "github.com/lightningnetwork/lnd/lnwire" + +// ZeroConfAcceptor wraps a regular ChainedAcceptor. If no acceptors are in the +// ChainedAcceptor, then Accept will reject all channel open requests. This +// should only be enabled when the zero-conf feature bit is set and is used to +// protect users from a malicious counter-party double-spending the zero-conf +// funding tx. +type ZeroConfAcceptor struct { + chainedAcceptor *ChainedAcceptor +} + +// NewZeroConfAcceptor initializes a ZeroConfAcceptor. +func NewZeroConfAcceptor() *ZeroConfAcceptor { + return &ZeroConfAcceptor{ + chainedAcceptor: NewChainedAcceptor(), + } +} + +// AddAcceptor adds a sub-ChannelAcceptor to the internal ChainedAcceptor. +func (z *ZeroConfAcceptor) AddAcceptor(acceptor ChannelAcceptor) uint64 { + return z.chainedAcceptor.AddAcceptor(acceptor) +} + +// RemoveAcceptor removes a sub-ChannelAcceptor from the internal +// ChainedAcceptor. +func (z *ZeroConfAcceptor) RemoveAcceptor(id uint64) { + z.chainedAcceptor.RemoveAcceptor(id) +} + +// Accept will deny the channel open request if the internal ChainedAcceptor is +// empty. If the internal ChainedAcceptor has any acceptors, then Accept will +// instead be called on it. +// +// NOTE: Part of the ChannelAcceptor interface. +func (z *ZeroConfAcceptor) Accept( + req *ChannelAcceptRequest) *ChannelAcceptResponse { + + // Alias for less verbosity. + channelType := req.OpenChanMsg.ChannelType + + // Check if the channel type sets the zero-conf bit. + var zeroConfSet bool + + if channelType != nil { + channelFeatures := lnwire.RawFeatureVector(*channelType) + zeroConfSet = channelFeatures.IsSet(lnwire.ZeroConfRequired) + } + + // If there are no acceptors and the counter-party is requesting a zero + // conf channel, reject the attempt. + if z.chainedAcceptor.numAcceptors() == 0 && zeroConfSet { + // Deny the channel open request. + rejectChannel := NewChannelAcceptResponse( + false, nil, nil, 0, 0, 0, 0, 0, 0, false, + ) + return rejectChannel + } + + // Otherwise, the ChainedAcceptor has sub-acceptors, so call Accept on + // it. + return z.chainedAcceptor.Accept(req) +} + +// A compile-time constraint to ensure ZeroConfAcceptor implements the +// MultiplexAcceptor interface. +var _ MultiplexAcceptor = (*ZeroConfAcceptor)(nil) diff --git a/chanacceptor/zeroconfacceptor_test.go b/chanacceptor/zeroconfacceptor_test.go new file mode 100644 index 000000000..f3e0496bf --- /dev/null +++ b/chanacceptor/zeroconfacceptor_test.go @@ -0,0 +1,83 @@ +package chanacceptor + +import ( + "testing" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// dummyAcceptor is a ChannelAcceptor that will never return a failure. +type dummyAcceptor struct{} + +func (d *dummyAcceptor) Accept( + req *ChannelAcceptRequest) *ChannelAcceptResponse { + + return &ChannelAcceptResponse{} +} + +// TestZeroConfAcceptorNormal verifies that the ZeroConfAcceptor will let +// requests go through for non-zero-conf channels if there are no +// sub-acceptors. +func TestZeroConfAcceptorNormal(t *testing.T) { + t.Parallel() + + // Create the zero-conf acceptor. + zeroAcceptor := NewZeroConfAcceptor() + + // Assert that calling Accept won't return a failure. + req := &ChannelAcceptRequest{ + OpenChanMsg: &lnwire.OpenChannel{}, + } + resp := zeroAcceptor.Accept(req) + require.False(t, resp.RejectChannel()) + + // Add a dummyAcceptor to the zero-conf acceptor. Assert that Accept + // does not return a failure. + dummy := &dummyAcceptor{} + dummyID := zeroAcceptor.AddAcceptor(dummy) + resp = zeroAcceptor.Accept(req) + require.False(t, resp.RejectChannel()) + + // Remove the dummyAcceptor from the zero-conf acceptor and assert that + // Accept doesn't return a failure. + zeroAcceptor.RemoveAcceptor(dummyID) + resp = zeroAcceptor.Accept(req) + require.False(t, resp.RejectChannel()) +} + +// TestZeroConfAcceptorZC verifies that the ZeroConfAcceptor will fail +// zero-conf channel opens unless a sub-acceptor exists. +func TestZeroConfAcceptorZC(t *testing.T) { + t.Parallel() + + // Create the zero-conf acceptor. + zeroAcceptor := NewZeroConfAcceptor() + + channelType := new(lnwire.ChannelType) + *channelType = lnwire.ChannelType(*lnwire.NewRawFeatureVector( + lnwire.ZeroConfRequired, + )) + + // Assert that calling Accept results in failure. + req := &ChannelAcceptRequest{ + OpenChanMsg: &lnwire.OpenChannel{ + ChannelType: channelType, + }, + } + resp := zeroAcceptor.Accept(req) + require.True(t, resp.RejectChannel()) + + // Add a dummyAcceptor to the zero-conf acceptor. Assert that Accept + // does not return a failure. + dummy := &dummyAcceptor{} + dummyID := zeroAcceptor.AddAcceptor(dummy) + resp = zeroAcceptor.Accept(req) + require.False(t, resp.RejectChannel()) + + // Remove the dummyAcceptor from the zero-conf acceptor and assert that + // Accept returns a failure. + zeroAcceptor.RemoveAcceptor(dummyID) + resp = zeroAcceptor.Accept(req) + require.True(t, resp.RejectChannel()) +} diff --git a/funding/manager.go b/funding/manager.go index cb0cdcaeb..56d575f4f 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1369,6 +1369,18 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, zeroConf = featureVec.IsSet(lnwire.ZeroConfRequired) scid = featureVec.IsSet(lnwire.ScidAliasRequired) + // If the zero-conf channel type was negotiated, ensure that + // the acceptor allows it. + if zeroConf && !acceptorResp.ZeroConf { + // Fail the funding flow. + flowErr := fmt.Errorf("channel acceptor blocked " + + "zero-conf channel negotiation") + f.failFundingFlow( + peer, msg.PendingChannelID, flowErr, + ) + return + } + // If the zero-conf channel type wasn't negotiated and the // fundee still wants a zero-conf channel, perform more checks. // Require that both sides have the scid-alias feature bit set. diff --git a/funding/manager_test.go b/funding/manager_test.go index 0b353d531..604844cd2 100644 --- a/funding/manager_test.go +++ b/funding/manager_test.go @@ -25,7 +25,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainreg" - "github.com/lightningnetwork/lnd/chanacceptor" + acpt "github.com/lightningnetwork/lnd/chanacceptor" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channelnotifier" "github.com/lightningnetwork/lnd/discovery" @@ -222,6 +222,19 @@ func (m *mockChanEvent) NotifyPendingOpenChannelEvent(outpoint wire.OutPoint, } } +// mockZeroConfAcceptor always accepts the channel open request for zero-conf +// channels. It will set the ZeroConf bool in the ChannelAcceptResponse. This +// is needed to properly unit test the zero-conf logic in the funding manager. +type mockZeroConfAcceptor struct{} + +func (m *mockZeroConfAcceptor) Accept( + req *acpt.ChannelAcceptRequest) *acpt.ChannelAcceptResponse { + + return &acpt.ChannelAcceptResponse{ + ZeroConf: true, + } +} + type newChannelMsg struct { channel *channeldb.OpenChannel err chan error @@ -400,7 +413,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey, var chanIDSeed [32]byte - chainedAcceptor := chanacceptor.NewChainedAcceptor() + chainedAcceptor := acpt.NewChainedAcceptor() fundingCfg := Config{ IDKey: privKey.PubKey(), @@ -556,7 +569,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) { oldCfg := alice.fundingMgr.cfg - chainedAcceptor := chanacceptor.NewChainedAcceptor() + chainedAcceptor := acpt.NewChainedAcceptor() f, err := NewFundingManager(Config{ IDKey: oldCfg.IDKey, @@ -3760,6 +3773,11 @@ func TestFundingManagerZeroConf(t *testing.T) { *lnwire.NewRawFeatureVector(channelTypeBits...), ) + // Create a default-accept channelacceptor so that the test passes and + // we don't have to use any goroutines. + mockAcceptor := &mockZeroConfAcceptor{} + bob.fundingMgr.cfg.OpenChannelPredicate = mockAcceptor + // Call fundChannel with the zero-conf ChannelType. fundingTx := fundChannel( t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true, diff --git a/lnd.go b/lnd.go index eb57995d2..25ece1d49 100644 --- a/lnd.go +++ b/lnd.go @@ -496,15 +496,22 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, } } - // Initialize the ChainedAcceptor. - chainedAcceptor := chanacceptor.NewChainedAcceptor() + // Initialize the MultiplexAcceptor. If lnd was started with the + // zero-conf feature bit, then this will be a ZeroConfAcceptor. + // Otherwise, this will be a ChainedAcceptor. + var multiAcceptor chanacceptor.MultiplexAcceptor + if cfg.ProtocolOptions.ZeroConf() { + multiAcceptor = chanacceptor.NewZeroConfAcceptor() + } else { + multiAcceptor = chanacceptor.NewChainedAcceptor() + } // Set up the core server which will listen for incoming peer // connections. server, err := newServer( cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc, activeChainControl.Cfg.WalletUnlockParams.ChansToRestore, - chainedAcceptor, torController, + multiAcceptor, torController, ) if err != nil { return mkErr("unable to create server: %v", err) @@ -534,7 +541,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg, // start the RPC server. err = rpcServer.addDeps( server, interceptorChain.MacaroonService(), cfg.SubRPCServers, - atplManager, server.invoices, tower, chainedAcceptor, + atplManager, server.invoices, tower, multiAcceptor, ) if err != nil { return mkErr("unable to add deps to RPC server: %v", err) diff --git a/rpcserver.go b/rpcserver.go index 01c9c9e15..43826c48b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -612,7 +612,7 @@ type rpcServer struct { // chanPredicate is used in the bidirectional ChannelAcceptor streaming // method. - chanPredicate *chanacceptor.ChainedAcceptor + chanPredicate chanacceptor.MultiplexAcceptor quit chan struct{} @@ -676,7 +676,7 @@ func newRPCServer(cfg *Config, interceptorChain *rpcperms.InterceptorChain, func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, subServerCgs *subRPCServerConfigs, atpl *autopilot.Manager, invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone, - chanPredicate *chanacceptor.ChainedAcceptor) error { + chanPredicate chanacceptor.MultiplexAcceptor) error { // Set up router rpc backend. selfNode, err := s.graphDB.SourceNode()