mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-21 22:11:41 +01:00
multi: add channel open parameters to channel acceptor
Add more fields to channel acceptor response so that users can have more fine grained control over their incoming channels. With our chained acceptor, it is possible that we get inconsistent responses from multiple chained acceptors. We create a conjugate repsponse from all the set fields in our various responses, but fail if we get different, non- zero responses from our various acceptors. Separate merge functions are used per type so that we avoid unexpected outcomes comparing interfaces (panic on comparing types that aren't comparable), with casting used where applicable to avoid code duplication.
This commit is contained in:
parent
0d35ce7561
commit
5679dde1bc
11 changed files with 1568 additions and 810 deletions
|
@ -7,9 +7,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testTimeout = time.Second
|
||||
|
@ -52,7 +56,7 @@ func newChanAcceptorCtx(t *testing.T, acceptCallCount int,
|
|||
|
||||
testCtx.acceptor = NewRPCAcceptor(
|
||||
testCtx.receiveResponse, testCtx.sendRequest, testTimeout*5,
|
||||
testCtx.quit,
|
||||
&chaincfg.TestNet3Params, testCtx.quit,
|
||||
)
|
||||
|
||||
return testCtx
|
||||
|
@ -157,6 +161,12 @@ func (c *channelAcceptorCtx) queryAndAssert(queries map[*lnwire.OpenChannel]*Cha
|
|||
// TestMultipleAcceptClients tests that the RPC acceptor is capable of handling
|
||||
// multiple requests to its Accept function and responding to them correctly.
|
||||
func TestMultipleAcceptClients(t *testing.T) {
|
||||
testAddr := "bcrt1qwrmq9uca0t3dy9t9wtuq5tm4405r7tfzyqn9pp"
|
||||
testUpfront, err := chancloser.ParseUpfrontShutdownAddress(
|
||||
testAddr, &chaincfg.TestNet3Params,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var (
|
||||
chan1 = &lnwire.OpenChannel{
|
||||
PendingChannelID: [32]byte{1},
|
||||
|
@ -173,17 +183,31 @@ func TestMultipleAcceptClients(t *testing.T) {
|
|||
// Queries is a map of the channel IDs we will query Accept
|
||||
// with, and the set of outcomes we expect.
|
||||
queries = map[*lnwire.OpenChannel]*ChannelAcceptResponse{
|
||||
chan1: NewChannelAcceptResponse(true, nil),
|
||||
chan2: NewChannelAcceptResponse(false, errChannelRejected),
|
||||
chan3: NewChannelAcceptResponse(false, customError),
|
||||
chan1: NewChannelAcceptResponse(
|
||||
true, nil, testUpfront, 1, 2, 3, 4, 5, 6,
|
||||
),
|
||||
chan2: NewChannelAcceptResponse(
|
||||
false, errChannelRejected, nil, 0, 0, 0,
|
||||
0, 0, 0,
|
||||
),
|
||||
chan3: NewChannelAcceptResponse(
|
||||
false, customError, nil, 0, 0, 0, 0, 0, 0,
|
||||
),
|
||||
}
|
||||
|
||||
// Responses is a mocked set of responses from the remote
|
||||
// channel acceptor.
|
||||
responses = map[[32]byte]*lnrpc.ChannelAcceptResponse{
|
||||
chan1.PendingChannelID: {
|
||||
PendingChanId: chan1.PendingChannelID[:],
|
||||
Accept: true,
|
||||
PendingChanId: chan1.PendingChannelID[:],
|
||||
Accept: true,
|
||||
UpfrontShutdown: testAddr,
|
||||
CsvDelay: 1,
|
||||
MaxHtlcCount: 2,
|
||||
MinAcceptDepth: 3,
|
||||
ReserveSat: 4,
|
||||
InFlightMaxMsat: 5,
|
||||
MinHtlcIn: 6,
|
||||
},
|
||||
chan2.PendingChannelID: {
|
||||
PendingChanId: chan2.PendingChannelID[:],
|
||||
|
@ -221,7 +245,8 @@ func TestInvalidResponse(t *testing.T) {
|
|||
{
|
||||
PendingChannelID: chan1,
|
||||
}: NewChannelAcceptResponse(
|
||||
false, errChannelRejected,
|
||||
false, errChannelRejected, nil, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -246,3 +271,46 @@ func TestInvalidResponse(t *testing.T) {
|
|||
// response, so we shutdown and assert here.
|
||||
testCtx.stop()
|
||||
}
|
||||
|
||||
// TestInvalidReserve tests validation of the channel reserve proposed by the
|
||||
// acceptor against the dust limit that was proposed by the remote peer.
|
||||
func TestInvalidReserve(t *testing.T) {
|
||||
var (
|
||||
chan1 = [32]byte{1}
|
||||
|
||||
dustLimit = btcutil.Amount(1000)
|
||||
reserve = dustLimit / 2
|
||||
|
||||
// We make a single query, and expect it to fail with our
|
||||
// generic error because channel reserve is too low.
|
||||
queries = map[*lnwire.OpenChannel]*ChannelAcceptResponse{
|
||||
{
|
||||
PendingChannelID: chan1,
|
||||
DustLimit: dustLimit,
|
||||
}: NewChannelAcceptResponse(
|
||||
false, errChannelRejected, nil, 0, 0,
|
||||
0, reserve, 0, 0,
|
||||
),
|
||||
}
|
||||
|
||||
// Create a single response which is invalid because the
|
||||
// proposed reserve is below our dust limit.
|
||||
responses = map[[32]byte]*lnrpc.ChannelAcceptResponse{
|
||||
chan1: {
|
||||
PendingChanId: chan1[:],
|
||||
Accept: true,
|
||||
ReserveSat: uint64(reserve),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Create and start our channel acceptor.
|
||||
testCtx := newChanAcceptorCtx(t, len(queries), responses)
|
||||
testCtx.start()
|
||||
|
||||
testCtx.queryAndAssert(queries)
|
||||
|
||||
// We do not expect our channel acceptor to exit because of one invalid
|
||||
// response, so we shutdown and assert here.
|
||||
testCtx.stop()
|
||||
}
|
||||
|
|
|
@ -50,6 +50,8 @@ func (c *ChainedAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptRespon
|
|||
c.acceptorsMtx.RLock()
|
||||
defer c.acceptorsMtx.RUnlock()
|
||||
|
||||
var finalResp ChannelAcceptResponse
|
||||
|
||||
for _, acceptor := range c.acceptors {
|
||||
// Call our acceptor to determine whether we want to accept this
|
||||
// channel.
|
||||
|
@ -61,11 +63,31 @@ func (c *ChainedAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptRespon
|
|||
if acceptorResponse.RejectChannel() {
|
||||
return acceptorResponse
|
||||
}
|
||||
|
||||
// If we have accepted the channel, we need to set the other
|
||||
// fields that were set in the response. However, since we are
|
||||
// dealing with multiple responses, we need to make sure that we
|
||||
// have not received inconsistent values (eg a csv delay of 1
|
||||
// from one acceptor, and a delay of 120 from another). We
|
||||
// set each value on our final response if it has not been set
|
||||
// yet, and allow duplicate sets if the value is the same. If
|
||||
// we cannot set a field, we return an error response.
|
||||
var err error
|
||||
finalResp, err = mergeResponse(finalResp, *acceptorResponse)
|
||||
if err != nil {
|
||||
log.Errorf("response for: %x has inconsistent values: %v",
|
||||
req.OpenChanMsg.PendingChannelID, err)
|
||||
|
||||
return NewChannelAcceptResponse(
|
||||
false, errChannelRejected, nil, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have gone through all of our acceptors with no objections, we
|
||||
// can return an acceptor with a nil error.
|
||||
return NewChannelAcceptResponse(true, nil)
|
||||
return &finalResp
|
||||
}
|
||||
|
||||
// A compile-time constraint to ensure ChainedAcceptor implements the
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
|
@ -28,11 +29,39 @@ type ChannelAcceptRequest struct {
|
|||
}
|
||||
|
||||
// ChannelAcceptResponse is a struct containing the response to a request to
|
||||
// open an inbound channel.
|
||||
// open an inbound channel. Note that fields added to this struct must be added
|
||||
// to the mergeResponse function to allow combining of responses from different
|
||||
// acceptors.
|
||||
type ChannelAcceptResponse struct {
|
||||
// ChanAcceptError the error returned by the channel acceptor. If the
|
||||
// channel was accepted, this value will be nil.
|
||||
ChanAcceptError
|
||||
|
||||
// UpfrontShutdown is the address that we will set as our upfront
|
||||
// shutdown address.
|
||||
UpfrontShutdown lnwire.DeliveryAddress
|
||||
|
||||
// CSVDelay is the csv delay we require for the remote peer.
|
||||
CSVDelay uint16
|
||||
|
||||
// Reserve is the amount that require the remote peer hold in reserve
|
||||
// on the channel.
|
||||
Reserve btcutil.Amount
|
||||
|
||||
// InFlightTotal is the maximum amount that we allow the remote peer to
|
||||
// hold in outstanding htlcs.
|
||||
InFlightTotal lnwire.MilliSatoshi
|
||||
|
||||
// HtlcLimit is the maximum number of htlcs that we allow the remote
|
||||
// peer to offer us.
|
||||
HtlcLimit uint16
|
||||
|
||||
// MinHtlcIn is the minimum incoming htlc value allowed on the channel.
|
||||
MinHtlcIn lnwire.MilliSatoshi
|
||||
|
||||
// MinAcceptDepth is the minimum depth that the initiator of the
|
||||
// channel should wait before considering the channel open.
|
||||
MinAcceptDepth uint16
|
||||
}
|
||||
|
||||
// NewChannelAcceptResponse is a constructor for a channel accept response,
|
||||
|
@ -40,13 +69,25 @@ type ChannelAcceptResponse struct {
|
|||
// a rejection) so that the error will be whitelisted and delivered to the
|
||||
// initiating peer. Accepted channels simply return a response containing a nil
|
||||
// error.
|
||||
func NewChannelAcceptResponse(accept bool,
|
||||
acceptErr error) *ChannelAcceptResponse {
|
||||
func NewChannelAcceptResponse(accept bool, acceptErr error,
|
||||
upfrontShutdown lnwire.DeliveryAddress, csvDelay, htlcLimit,
|
||||
minDepth uint16, reserve btcutil.Amount, inFlight,
|
||||
minHtlcIn lnwire.MilliSatoshi) *ChannelAcceptResponse {
|
||||
|
||||
resp := &ChannelAcceptResponse{
|
||||
UpfrontShutdown: upfrontShutdown,
|
||||
CSVDelay: csvDelay,
|
||||
Reserve: reserve,
|
||||
InFlightTotal: inFlight,
|
||||
HtlcLimit: htlcLimit,
|
||||
MinHtlcIn: minHtlcIn,
|
||||
MinAcceptDepth: minDepth,
|
||||
}
|
||||
|
||||
// If we want to accept the channel, we return a response with a nil
|
||||
// error.
|
||||
if accept {
|
||||
return &ChannelAcceptResponse{}
|
||||
return resp
|
||||
}
|
||||
|
||||
// Use a generic error when no custom error is provided.
|
||||
|
@ -54,11 +95,11 @@ func NewChannelAcceptResponse(accept bool,
|
|||
acceptErr = errChannelRejected
|
||||
}
|
||||
|
||||
return &ChannelAcceptResponse{
|
||||
ChanAcceptError: ChanAcceptError{
|
||||
error: acceptErr,
|
||||
},
|
||||
resp.ChanAcceptError = ChanAcceptError{
|
||||
error: acceptErr,
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// RejectChannel returns a boolean that indicates whether we should reject the
|
||||
|
|
152
chanacceptor/merge.go
Normal file
152
chanacceptor/merge.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package chanacceptor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
const (
|
||||
// We use field names in our errors for more readable errors. Create
|
||||
// consts for them here so that we can exactly match in our unit tests.
|
||||
fieldCSV = "csv delay"
|
||||
fieldHtlcLimit = "htlc limit"
|
||||
fieldMinDep = "min depth"
|
||||
fieldReserve = "reserve"
|
||||
fieldMinIn = "min htlc in"
|
||||
fieldInFlightTotal = "in flight total"
|
||||
fieldUpfrontShutdown = "upfront shutdown"
|
||||
)
|
||||
|
||||
// fieldMismatchError returns a merge error for a named field when we get two
|
||||
// channel acceptor responses which have different values set.
|
||||
func fieldMismatchError(name string, current, new interface{}) error {
|
||||
return fmt.Errorf("multiple values set for: %v, %v and %v",
|
||||
name, current, new)
|
||||
}
|
||||
|
||||
// mergeInt64 merges two int64 values, failing if they have different non-zero
|
||||
// values.
|
||||
func mergeInt64(name string, current, new int64) (int64, error) {
|
||||
switch {
|
||||
case current == 0:
|
||||
return new, nil
|
||||
|
||||
case new == 0:
|
||||
return current, nil
|
||||
|
||||
case current != new:
|
||||
return 0, fieldMismatchError(name, current, new)
|
||||
|
||||
default:
|
||||
return new, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mergeMillisatoshi merges two msat values, failing if they have different
|
||||
// non-zero values.
|
||||
func mergeMillisatoshi(name string, current,
|
||||
new lnwire.MilliSatoshi) (lnwire.MilliSatoshi, error) {
|
||||
|
||||
switch {
|
||||
case current == 0:
|
||||
return new, nil
|
||||
|
||||
case new == 0:
|
||||
return current, nil
|
||||
|
||||
case current != new:
|
||||
return 0, fieldMismatchError(name, current, new)
|
||||
|
||||
default:
|
||||
return new, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mergeDeliveryAddress merges two delivery address values, failing if they have
|
||||
// different non-zero values.
|
||||
func mergeDeliveryAddress(name string, current,
|
||||
new lnwire.DeliveryAddress) (lnwire.DeliveryAddress, error) {
|
||||
|
||||
switch {
|
||||
case current == nil:
|
||||
return new, nil
|
||||
|
||||
case new == nil:
|
||||
return current, nil
|
||||
|
||||
case !bytes.Equal(current, new):
|
||||
return nil, fieldMismatchError(name, current, new)
|
||||
|
||||
default:
|
||||
return new, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mergeResponse takes two channel accept responses, and attempts to merge their
|
||||
// fields, failing if any fields conflict (are non-zero and not equal). It
|
||||
// returns a new response that has all the merged fields in it.
|
||||
func mergeResponse(current, new ChannelAcceptResponse) (ChannelAcceptResponse,
|
||||
error) {
|
||||
|
||||
csv, err := mergeInt64(
|
||||
fieldCSV, int64(current.CSVDelay), int64(new.CSVDelay),
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
current.CSVDelay = uint16(csv)
|
||||
|
||||
htlcLimit, err := mergeInt64(
|
||||
fieldHtlcLimit, int64(current.HtlcLimit),
|
||||
int64(new.HtlcLimit),
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
current.HtlcLimit = uint16(htlcLimit)
|
||||
|
||||
minDepth, err := mergeInt64(
|
||||
fieldMinDep, int64(current.MinAcceptDepth),
|
||||
int64(new.MinAcceptDepth),
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
current.MinAcceptDepth = uint16(minDepth)
|
||||
|
||||
reserve, err := mergeInt64(
|
||||
fieldReserve, int64(current.Reserve), int64(new.Reserve),
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
current.Reserve = btcutil.Amount(reserve)
|
||||
|
||||
current.MinHtlcIn, err = mergeMillisatoshi(
|
||||
fieldMinIn, current.MinHtlcIn, new.MinHtlcIn,
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
|
||||
current.InFlightTotal, err = mergeMillisatoshi(
|
||||
fieldInFlightTotal, current.InFlightTotal,
|
||||
new.InFlightTotal,
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
|
||||
current.UpfrontShutdown, err = mergeDeliveryAddress(
|
||||
fieldUpfrontShutdown, current.UpfrontShutdown,
|
||||
new.UpfrontShutdown,
|
||||
)
|
||||
if err != nil {
|
||||
return current, err
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
188
chanacceptor/merge_test.go
Normal file
188
chanacceptor/merge_test.go
Normal file
|
@ -0,0 +1,188 @@
|
|||
package chanacceptor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMergeResponse tests merging of channel acceptor responses.
|
||||
func TestMergeResponse(t *testing.T) {
|
||||
var (
|
||||
addr1 = lnwire.DeliveryAddress{1}
|
||||
addr2 = lnwire.DeliveryAddress{2}
|
||||
|
||||
populatedResp = ChannelAcceptResponse{
|
||||
UpfrontShutdown: addr1,
|
||||
CSVDelay: 2,
|
||||
Reserve: 3,
|
||||
InFlightTotal: 4,
|
||||
HtlcLimit: 5,
|
||||
MinHtlcIn: 6,
|
||||
MinAcceptDepth: 7,
|
||||
}
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
current ChannelAcceptResponse
|
||||
new ChannelAcceptResponse
|
||||
merged ChannelAcceptResponse
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "same response",
|
||||
current: populatedResp,
|
||||
new: populatedResp,
|
||||
merged: populatedResp,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "different upfront",
|
||||
current: ChannelAcceptResponse{
|
||||
UpfrontShutdown: addr1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
UpfrontShutdown: addr2,
|
||||
},
|
||||
err: fieldMismatchError(fieldUpfrontShutdown, addr1, addr2),
|
||||
},
|
||||
{
|
||||
name: "different csv",
|
||||
current: ChannelAcceptResponse{
|
||||
CSVDelay: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
CSVDelay: 2,
|
||||
},
|
||||
err: fieldMismatchError(fieldCSV, 1, 2),
|
||||
},
|
||||
{
|
||||
name: "different reserve",
|
||||
current: ChannelAcceptResponse{
|
||||
Reserve: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
Reserve: 2,
|
||||
},
|
||||
err: fieldMismatchError(fieldReserve, 1, 2),
|
||||
},
|
||||
{
|
||||
name: "different in flight",
|
||||
current: ChannelAcceptResponse{
|
||||
InFlightTotal: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
InFlightTotal: 2,
|
||||
},
|
||||
err: fieldMismatchError(
|
||||
fieldInFlightTotal, lnwire.MilliSatoshi(1),
|
||||
lnwire.MilliSatoshi(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "different htlc limit",
|
||||
current: ChannelAcceptResponse{
|
||||
HtlcLimit: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
HtlcLimit: 2,
|
||||
},
|
||||
err: fieldMismatchError(fieldHtlcLimit, 1, 2),
|
||||
},
|
||||
{
|
||||
name: "different min in",
|
||||
current: ChannelAcceptResponse{
|
||||
MinHtlcIn: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
MinHtlcIn: 2,
|
||||
},
|
||||
err: fieldMismatchError(
|
||||
fieldMinIn, lnwire.MilliSatoshi(1),
|
||||
lnwire.MilliSatoshi(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "different depth",
|
||||
current: ChannelAcceptResponse{
|
||||
MinAcceptDepth: 1,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
MinAcceptDepth: 2,
|
||||
},
|
||||
err: fieldMismatchError(fieldMinDep, 1, 2),
|
||||
},
|
||||
{
|
||||
name: "merge all values",
|
||||
current: ChannelAcceptResponse{
|
||||
UpfrontShutdown: lnwire.DeliveryAddress{1},
|
||||
CSVDelay: 1,
|
||||
Reserve: 0,
|
||||
InFlightTotal: 3,
|
||||
HtlcLimit: 0,
|
||||
MinHtlcIn: 5,
|
||||
MinAcceptDepth: 0,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
UpfrontShutdown: nil,
|
||||
CSVDelay: 0,
|
||||
Reserve: 2,
|
||||
InFlightTotal: 0,
|
||||
HtlcLimit: 4,
|
||||
MinHtlcIn: 0,
|
||||
MinAcceptDepth: 6,
|
||||
},
|
||||
merged: ChannelAcceptResponse{
|
||||
UpfrontShutdown: lnwire.DeliveryAddress{1},
|
||||
CSVDelay: 1,
|
||||
Reserve: 2,
|
||||
InFlightTotal: 3,
|
||||
HtlcLimit: 4,
|
||||
MinHtlcIn: 5,
|
||||
MinAcceptDepth: 6,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
// Test the case where fields have the same non-zero
|
||||
// value, and the case where only response value is
|
||||
// non-zero.
|
||||
name: "empty and identical",
|
||||
current: ChannelAcceptResponse{
|
||||
CSVDelay: 1,
|
||||
Reserve: 2,
|
||||
InFlightTotal: 0,
|
||||
},
|
||||
new: ChannelAcceptResponse{
|
||||
CSVDelay: 0,
|
||||
Reserve: 2,
|
||||
InFlightTotal: 3,
|
||||
},
|
||||
merged: ChannelAcceptResponse{
|
||||
CSVDelay: 1,
|
||||
Reserve: 2,
|
||||
InFlightTotal: 3,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resp, err := mergeResponse(test.current, test.new)
|
||||
require.Equal(t, test.err, err)
|
||||
|
||||
// If we expect an error, exit early rather than compare
|
||||
// our result.
|
||||
if test.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(t, test.merged, resp)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,12 +1,18 @@
|
|||
package chanacceptor
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -17,11 +23,26 @@ var (
|
|||
errCustomLength = fmt.Errorf("custom error message exceeds length "+
|
||||
"limit: %v", maxErrorLength)
|
||||
|
||||
// errInvalidUpfrontShutdown is returned when we cannot parse the
|
||||
// upfront shutdown address returned.
|
||||
errInvalidUpfrontShutdown = fmt.Errorf("could not parse upfront " +
|
||||
"shutdown address")
|
||||
|
||||
// errInsufficientReserve is returned when the reserve proposed by for
|
||||
// a channel is less than the dust limit originally supplied.
|
||||
errInsufficientReserve = fmt.Errorf("reserve lower than proposed dust " +
|
||||
"limit")
|
||||
|
||||
// errAcceptWithError is returned when we get a response which accepts
|
||||
// a channel but ambiguously also sets a custom error message.
|
||||
errAcceptWithError = errors.New("channel acceptor response accepts " +
|
||||
"channel, but also includes custom error")
|
||||
|
||||
// errMaxHtlcTooHigh is returned if our htlc count exceeds the number
|
||||
// hard-set by BOLT 2.
|
||||
errMaxHtlcTooHigh = fmt.Errorf("htlc limit exceeds spec limit of: %v",
|
||||
input.MaxHTLCNumber/2)
|
||||
|
||||
// maxErrorLength is the maximum error length we allow the error we
|
||||
// send to our peer to be.
|
||||
maxErrorLength = 500
|
||||
|
@ -54,6 +75,9 @@ type RPCAcceptor struct {
|
|||
// acceptor, and the time it takes to receive a response.
|
||||
timeout time.Duration
|
||||
|
||||
// params are our current chain params.
|
||||
params *chaincfg.Params
|
||||
|
||||
// done is closed when the rpc client terminates.
|
||||
done chan struct{}
|
||||
|
||||
|
@ -83,7 +107,7 @@ func (r *RPCAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptResponse {
|
|||
// Create a rejection response which we can use for the cases where we
|
||||
// reject the channel.
|
||||
rejectChannel := NewChannelAcceptResponse(
|
||||
false, errChannelRejected,
|
||||
false, errChannelRejected, nil, 0, 0, 0, 0, 0, 0,
|
||||
)
|
||||
|
||||
// Send the request to the newRequests channel.
|
||||
|
@ -123,14 +147,15 @@ func (r *RPCAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptResponse {
|
|||
|
||||
// NewRPCAcceptor creates and returns an instance of the RPCAcceptor.
|
||||
func NewRPCAcceptor(receive func() (*lnrpc.ChannelAcceptResponse, error),
|
||||
send func(*lnrpc.ChannelAcceptRequest) error,
|
||||
timeout time.Duration, quit chan struct{}) *RPCAcceptor {
|
||||
send func(*lnrpc.ChannelAcceptRequest) error, timeout time.Duration,
|
||||
params *chaincfg.Params, quit chan struct{}) *RPCAcceptor {
|
||||
|
||||
return &RPCAcceptor{
|
||||
receive: receive,
|
||||
send: send,
|
||||
requests: make(chan *chanAcceptInfo),
|
||||
timeout: timeout,
|
||||
params: params,
|
||||
done: make(chan struct{}),
|
||||
quit: quit,
|
||||
}
|
||||
|
@ -181,9 +206,16 @@ func (r *RPCAcceptor) receiveResponses(errChan chan error,
|
|||
copy(pendingID[:], resp.PendingChanId)
|
||||
|
||||
openChanResp := lnrpc.ChannelAcceptResponse{
|
||||
Accept: resp.Accept,
|
||||
PendingChanId: pendingID[:],
|
||||
Error: resp.Error,
|
||||
Accept: resp.Accept,
|
||||
PendingChanId: pendingID[:],
|
||||
Error: resp.Error,
|
||||
UpfrontShutdown: resp.UpfrontShutdown,
|
||||
CsvDelay: resp.CsvDelay,
|
||||
ReserveSat: resp.ReserveSat,
|
||||
InFlightMaxMsat: resp.InFlightMaxMsat,
|
||||
MaxHtlcCount: resp.MaxHtlcCount,
|
||||
MinHtlcIn: resp.MinHtlcIn,
|
||||
MinAcceptDepth: resp.MinAcceptDepth,
|
||||
}
|
||||
|
||||
// We have received a decision for one of our channel
|
||||
|
@ -210,7 +242,10 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||
// listening and any in-progress requests should be terminated.
|
||||
defer close(r.done)
|
||||
|
||||
acceptRequests := make(map[[32]byte]chan *ChannelAcceptResponse)
|
||||
// Create a map of pending channel IDs to our original open channel
|
||||
// request and a response channel. We keep the original chanel open
|
||||
// message so that we can validate our response against it.
|
||||
acceptRequests := make(map[[32]byte]*chanAcceptInfo)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
@ -221,7 +256,7 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||
req := newRequest.request
|
||||
pendingChanID := req.OpenChanMsg.PendingChannelID
|
||||
|
||||
acceptRequests[pendingChanID] = newRequest.response
|
||||
acceptRequests[pendingChanID] = newRequest
|
||||
|
||||
// A ChannelAcceptRequest has been received, send it to the client.
|
||||
chanAcceptReq := &lnrpc.ChannelAcceptRequest{
|
||||
|
@ -253,7 +288,7 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||
// over it.
|
||||
var pendingID [32]byte
|
||||
copy(pendingID[:], resp.PendingChanId)
|
||||
respChan, ok := acceptRequests[pendingID]
|
||||
requestInfo, ok := acceptRequests[pendingID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
@ -261,14 +296,22 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||
// Validate the response we have received. If it is not
|
||||
// valid, we log our error and proceed to deliver the
|
||||
// rejection.
|
||||
accept, acceptErr, err := validateAcceptorResponse(resp)
|
||||
accept, acceptErr, shutdown, err := r.validateAcceptorResponse(
|
||||
requestInfo.request.OpenChanMsg.DustLimit, resp,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid acceptor response: %v", err)
|
||||
}
|
||||
|
||||
// Send the response boolean over the buffered response
|
||||
// channel.
|
||||
respChan <- NewChannelAcceptResponse(accept, acceptErr)
|
||||
requestInfo.response <- NewChannelAcceptResponse(
|
||||
accept, acceptErr, shutdown,
|
||||
uint16(resp.CsvDelay),
|
||||
uint16(resp.MaxHtlcCount),
|
||||
uint16(resp.MinAcceptDepth),
|
||||
btcutil.Amount(resp.ReserveSat),
|
||||
lnwire.MilliSatoshi(resp.InFlightMaxMsat),
|
||||
lnwire.MilliSatoshi(resp.MinHtlcIn),
|
||||
)
|
||||
|
||||
// Delete the channel from the acceptRequests map.
|
||||
delete(acceptRequests, pendingID)
|
||||
|
@ -288,12 +331,49 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||
// validateAcceptorResponse validates the response we get from the channel
|
||||
// acceptor, returning a boolean indicating whether to accept the channel, an
|
||||
// error to send to the peer, and any validation errors that occurred.
|
||||
func validateAcceptorResponse(req lnrpc.ChannelAcceptResponse) (bool, error,
|
||||
func (r *RPCAcceptor) validateAcceptorResponse(dustLimit btcutil.Amount,
|
||||
req lnrpc.ChannelAcceptResponse) (bool, error, lnwire.DeliveryAddress,
|
||||
error) {
|
||||
|
||||
channelStr := hex.EncodeToString(req.PendingChanId)
|
||||
|
||||
// Check that the max htlc count is within the BOLT 2 hard-limit of 483.
|
||||
// The initiating side should fail values above this anyway, but we
|
||||
// catch the invalid user input here.
|
||||
if req.MaxHtlcCount > input.MaxHTLCNumber/2 {
|
||||
log.Errorf("Max htlc count: %v for channel: %v is greater "+
|
||||
"than limit of: %v", req.MaxHtlcCount, channelStr,
|
||||
input.MaxHTLCNumber/2)
|
||||
|
||||
return false, errChannelRejected, nil, errMaxHtlcTooHigh
|
||||
}
|
||||
|
||||
// Ensure that the reserve that has been proposed, if it is set, is at
|
||||
// least the dust limit that was proposed by the remote peer. This is
|
||||
// required by BOLT 2.
|
||||
reserveSat := btcutil.Amount(req.ReserveSat)
|
||||
if reserveSat != 0 && reserveSat < dustLimit {
|
||||
log.Errorf("Remote reserve: %v sat for channel: %v must be "+
|
||||
"at least equal to proposed dust limit: %v",
|
||||
req.ReserveSat, channelStr, dustLimit)
|
||||
|
||||
return false, errChannelRejected, nil, errInsufficientReserve
|
||||
}
|
||||
|
||||
// Attempt to parse the upfront shutdown address provided.
|
||||
upfront, err := chancloser.ParseUpfrontShutdownAddress(
|
||||
req.UpfrontShutdown, r.params,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Could not parse upfront shutdown for "+
|
||||
"%v: %v", channelStr, err)
|
||||
|
||||
return false, errChannelRejected, nil, errInvalidUpfrontShutdown
|
||||
}
|
||||
|
||||
// Check that the custom error provided is valid.
|
||||
if len(req.Error) > maxErrorLength {
|
||||
return false, errChannelRejected, errCustomLength
|
||||
return false, errChannelRejected, nil, errCustomLength
|
||||
}
|
||||
|
||||
var haveCustomError = len(req.Error) != 0
|
||||
|
@ -302,21 +382,21 @@ func validateAcceptorResponse(req lnrpc.ChannelAcceptResponse) (bool, error,
|
|||
// If accept is true, but we also have an error specified, we fail
|
||||
// because this result is ambiguous.
|
||||
case req.Accept && haveCustomError:
|
||||
return false, errChannelRejected, errAcceptWithError
|
||||
return false, errChannelRejected, nil, errAcceptWithError
|
||||
|
||||
// If we accept without an error message, we can just return a nil
|
||||
// error.
|
||||
case req.Accept:
|
||||
return true, nil, nil
|
||||
return true, nil, upfront, nil
|
||||
|
||||
// If we reject the channel, and have a custom error, then we use it.
|
||||
case haveCustomError:
|
||||
return false, fmt.Errorf(req.Error), nil
|
||||
return false, fmt.Errorf(req.Error), nil, nil
|
||||
|
||||
// Otherwise, we have rejected the channel with no custom error, so we
|
||||
// just use a generic error to fail the channel.
|
||||
default:
|
||||
return false, errChannelRejected, nil
|
||||
return false, errChannelRejected, nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,33 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestValidateAcceptorResponse test validation of acceptor responses.
|
||||
func TestValidateAcceptorResponse(t *testing.T) {
|
||||
customError := errors.New("custom error")
|
||||
var (
|
||||
customError = errors.New("custom error")
|
||||
validAddr = "bcrt1qwrmq9uca0t3dy9t9wtuq5tm4405r7tfzyqn9pp"
|
||||
addr, _ = chancloser.ParseUpfrontShutdownAddress(
|
||||
validAddr, &chaincfg.TestNet3Params,
|
||||
)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dustLimit btcutil.Amount
|
||||
response lnrpc.ChannelAcceptResponse
|
||||
accept bool
|
||||
acceptorErr error
|
||||
error error
|
||||
shutdown lnwire.DeliveryAddress
|
||||
}{
|
||||
{
|
||||
name: "accepted with error",
|
||||
|
@ -43,11 +56,13 @@ func TestValidateAcceptorResponse(t *testing.T) {
|
|||
{
|
||||
name: "accepted",
|
||||
response: lnrpc.ChannelAcceptResponse{
|
||||
Accept: true,
|
||||
Accept: true,
|
||||
UpfrontShutdown: validAddr,
|
||||
},
|
||||
accept: true,
|
||||
acceptorErr: nil,
|
||||
error: nil,
|
||||
shutdown: addr,
|
||||
},
|
||||
{
|
||||
name: "rejected with error",
|
||||
|
@ -68,18 +83,57 @@ func TestValidateAcceptorResponse(t *testing.T) {
|
|||
acceptorErr: errChannelRejected,
|
||||
error: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid upfront shutdown",
|
||||
response: lnrpc.ChannelAcceptResponse{
|
||||
Accept: true,
|
||||
UpfrontShutdown: "invalid addr",
|
||||
},
|
||||
accept: false,
|
||||
acceptorErr: errChannelRejected,
|
||||
error: errInvalidUpfrontShutdown,
|
||||
},
|
||||
{
|
||||
name: "reserve too low",
|
||||
dustLimit: 100,
|
||||
response: lnrpc.ChannelAcceptResponse{
|
||||
Accept: true,
|
||||
ReserveSat: 10,
|
||||
},
|
||||
accept: false,
|
||||
acceptorErr: errChannelRejected,
|
||||
error: errInsufficientReserve,
|
||||
},
|
||||
{
|
||||
name: "max htlcs too high",
|
||||
dustLimit: 100,
|
||||
response: lnrpc.ChannelAcceptResponse{
|
||||
Accept: true,
|
||||
MaxHtlcCount: 1 + input.MaxHTLCNumber/2,
|
||||
},
|
||||
accept: false,
|
||||
acceptorErr: errChannelRejected,
|
||||
error: errMaxHtlcTooHigh,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
accept, acceptErr, err := validateAcceptorResponse(
|
||||
test.response,
|
||||
// Create an acceptor, everything can be nil because
|
||||
// we just need the params.
|
||||
acceptor := NewRPCAcceptor(
|
||||
nil, nil, 0, &chaincfg.TestNet3Params, nil,
|
||||
)
|
||||
|
||||
accept, acceptErr, shutdown, err := acceptor.validateAcceptorResponse(
|
||||
test.dustLimit, test.response,
|
||||
)
|
||||
require.Equal(t, test.accept, accept)
|
||||
require.Equal(t, test.acceptorErr, acceptErr)
|
||||
require.Equal(t, test.error, err)
|
||||
require.Equal(t, test.shutdown, shutdown)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1340,8 +1340,12 @@ func (f *fundingManager) handleFundingOpen(peer lnpeer.Peer,
|
|||
// that we require before both of us consider the channel open. We'll
|
||||
// use our mapping to derive the proper number of confirmations based on
|
||||
// the amount of the channel, and also if any funds are being pushed to
|
||||
// us.
|
||||
// us. If a depth value was set by our channel acceptor, we will use
|
||||
// that value instead.
|
||||
numConfsReq := f.cfg.NumRequiredConfs(msg.FundingAmount, msg.PushAmount)
|
||||
if acceptorResp.MinAcceptDepth != 0 {
|
||||
numConfsReq = acceptorResp.MinAcceptDepth
|
||||
}
|
||||
reservation.SetNumConfsRequired(numConfsReq)
|
||||
|
||||
// We'll also validate and apply all the constraints the initiating
|
||||
|
@ -1365,10 +1369,10 @@ func (f *fundingManager) handleFundingOpen(peer lnpeer.Peer,
|
|||
|
||||
// Check whether the peer supports upfront shutdown, and get a new wallet
|
||||
// address if our node is configured to set shutdown addresses by default.
|
||||
// A nil address is set in place of user input, because this channel open
|
||||
// was not initiated by the user.
|
||||
// We use the upfront shutdown script provided by our channel acceptor
|
||||
// (if any) in lieu of user input.
|
||||
shutdown, err := getUpfrontShutdownScript(
|
||||
f.cfg.EnableUpfrontShutdown, peer, nil,
|
||||
f.cfg.EnableUpfrontShutdown, peer, acceptorResp.UpfrontShutdown,
|
||||
func() (lnwire.DeliveryAddress, error) {
|
||||
addr, err := f.cfg.Wallet.NewAddress(lnwallet.WitnessPubKey, false)
|
||||
if err != nil {
|
||||
|
@ -1391,12 +1395,34 @@ func (f *fundingManager) handleFundingOpen(peer lnpeer.Peer,
|
|||
msg.PendingChannelID, amt, msg.PushAmount,
|
||||
commitType, msg.UpfrontShutdownScript)
|
||||
|
||||
// Generate our required constraints for the remote party.
|
||||
// Generate our required constraints for the remote party, using the
|
||||
// values provided by the channel acceptor if they are non-zero.
|
||||
remoteCsvDelay := f.cfg.RequiredRemoteDelay(amt)
|
||||
if acceptorResp.CSVDelay != 0 {
|
||||
remoteCsvDelay = acceptorResp.CSVDelay
|
||||
}
|
||||
|
||||
chanReserve := f.cfg.RequiredRemoteChanReserve(amt, msg.DustLimit)
|
||||
if acceptorResp.Reserve != 0 {
|
||||
chanReserve = acceptorResp.Reserve
|
||||
}
|
||||
|
||||
remoteMaxValue := f.cfg.RequiredRemoteMaxValue(amt)
|
||||
if acceptorResp.InFlightTotal != 0 {
|
||||
remoteMaxValue = acceptorResp.InFlightTotal
|
||||
}
|
||||
|
||||
maxHtlcs := f.cfg.RequiredRemoteMaxHTLCs(amt)
|
||||
if acceptorResp.HtlcLimit != 0 {
|
||||
maxHtlcs = acceptorResp.HtlcLimit
|
||||
}
|
||||
|
||||
// Default to our default minimum hltc value, replacing it with the
|
||||
// channel acceptor's value if it is set.
|
||||
minHtlc := f.cfg.DefaultMinHtlcIn
|
||||
if acceptorResp.MinHtlcIn != 0 {
|
||||
minHtlc = acceptorResp.MinHtlcIn
|
||||
}
|
||||
|
||||
// Once the reservation has been created successfully, we add it to
|
||||
// this peer's map of pending reservations to track this particular
|
||||
|
|
1614
lnrpc/rpc.pb.go
1614
lnrpc/rpc.pb.go
File diff suppressed because it is too large
Load diff
|
@ -801,6 +801,48 @@ message ChannelAcceptResponse {
|
|||
characters.
|
||||
*/
|
||||
string error = 3;
|
||||
|
||||
/*
|
||||
The upfront shutdown address to use if the initiating peer supports option
|
||||
upfront shutdown script (see ListPeers for the features supported). Note
|
||||
that the channel open will fail if this value is set for a peer that does
|
||||
not support this feature bit.
|
||||
*/
|
||||
string upfront_shutdown = 4;
|
||||
|
||||
/*
|
||||
The csv delay (in blocks) that we require for the remote party.
|
||||
*/
|
||||
uint32 csv_delay = 5;
|
||||
|
||||
/*
|
||||
The reserve amount in satoshis that we require the remote peer to adhere to.
|
||||
We require that the remote peer always have some reserve amount allocated to
|
||||
them so that there is always a disincentive to broadcast old state (if they
|
||||
hold 0 sats on their side of the channel, there is nothing to lose).
|
||||
*/
|
||||
uint64 reserve_sat = 6;
|
||||
|
||||
/*
|
||||
The maximum amount of funds in millisatoshis that we allow the remote peer
|
||||
to have in outstanding htlcs.
|
||||
*/
|
||||
uint64 in_flight_max_msat = 7;
|
||||
|
||||
/*
|
||||
The maximum number of htlcs that the remote peer can offer us.
|
||||
*/
|
||||
uint32 max_htlc_count = 8;
|
||||
|
||||
/*
|
||||
The minimum value in millisatoshis for incoming htlcs on the channel.
|
||||
*/
|
||||
uint64 min_htlc_in = 9;
|
||||
|
||||
/*
|
||||
The number of confirmations we require before we consider the channel open.
|
||||
*/
|
||||
uint32 min_accept_depth = 10;
|
||||
}
|
||||
|
||||
message ChannelPoint {
|
||||
|
|
|
@ -6391,7 +6391,8 @@ func (r *rpcServer) ChannelAcceptor(stream lnrpc.Lightning_ChannelAcceptorServer
|
|||
// Create a new RPCAcceptor which will send requests into the
|
||||
// newRequests channel when it receives them.
|
||||
rpcAcceptor := chanacceptor.NewRPCAcceptor(
|
||||
stream.Recv, stream.Send, r.cfg.AcceptorTimeout, r.quit,
|
||||
stream.Recv, stream.Send, r.cfg.AcceptorTimeout,
|
||||
r.cfg.ActiveNetParams.Params, r.quit,
|
||||
)
|
||||
|
||||
// Add the RPCAcceptor to the ChainedAcceptor and defer its removal.
|
||||
|
|
Loading…
Add table
Reference in a new issue