Skip to content

Commit

Permalink
Merge pull request lightningnetwork#6716 from Crypt-iQ/zeroconfacceptor
Browse files Browse the repository at this point in the history
multi: add zeroconfacceptor to default reject incoming channels
  • Loading branch information
Roasbeef authored Aug 12, 2022
2 parents d0996a9 + dd0615f commit be5bc79
Show file tree
Hide file tree
Showing 17 changed files with 2,998 additions and 2,571 deletions.
16 changes: 14 additions & 2 deletions chanacceptor/chainedacceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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.
//
Expand Down Expand Up @@ -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)
13 changes: 13 additions & 0 deletions chanacceptor/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
73 changes: 71 additions & 2 deletions chanacceptor/rpcacceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,20 +258,73 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
pendingChanID := req.OpenChanMsg.PendingChannelID

// Map the channel commitment type to its RPC
// counterpart.
var commitmentType lnrpc.CommitmentType
// counterpart. Also determine whether the zero-conf or
// scid-alias channel types are set.
var (
commitmentType lnrpc.CommitmentType
wantsZeroConf bool
wantsScidAlias bool
)

if req.OpenChanMsg.ChannelType != nil {
channelFeatures := lnwire.RawFeatureVector(
*req.OpenChanMsg.ChannelType,
)
switch {
case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScidAliasRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE

case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE

case channelFeatures.OnlyContains(
lnwire.ScidAliasRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE

case channelFeatures.OnlyContains(
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE

case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScidAliasRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_ANCHORS

case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_ANCHORS

case channelFeatures.OnlyContains(
lnwire.ScidAliasRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
commitmentType = lnrpc.CommitmentType_ANCHORS

case channelFeatures.OnlyContains(
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
Expand All @@ -291,6 +344,20 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
"in channel acceptor request: %v",
req.OpenChanMsg.ChannelType)
}

if channelFeatures.IsSet(
lnwire.ZeroConfRequired,
) {

wantsZeroConf = true
}

if channelFeatures.IsSet(
lnwire.ScidAliasRequired,
) {

wantsScidAlias = true
}
}

acceptRequests[pendingChanID] = newRequest
Expand All @@ -311,6 +378,8 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
MaxAcceptedHtlcs: uint32(req.OpenChanMsg.MaxAcceptedHTLCs),
ChannelFlags: uint32(req.OpenChanMsg.ChannelFlags),
CommitmentType: commitmentType,
WantsZeroConf: wantsZeroConf,
WantsScidAlias: wantsScidAlias,
}

if err := r.send(chanAcceptReq); err != nil {
Expand Down
68 changes: 68 additions & 0 deletions chanacceptor/zeroconfacceptor.go
Original file line number Diff line number Diff line change
@@ -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)
83 changes: 83 additions & 0 deletions chanacceptor/zeroconfacceptor_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
3 changes: 3 additions & 0 deletions docs/release-notes/release-notes-0.15.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
information. A new `listaliases` API has also been added that returns a data dump of all
existing alias info.](https://github.com/lightningnetwork/lnd/pull/6734)

* [Adds a `ZeroConfAcceptor` that rejects any zero-conf channel opens unless an RPC `ChannelAcceptor` is
active. This is a safety measure to avoid funds loss.](https://github.com/lightningnetwork/lnd/pull/6716)

## Build system

* [Add the release build directory to the `.gitignore` file to avoid the release
Expand Down
12 changes: 12 additions & 0 deletions funding/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit be5bc79

Please sign in to comment.