Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lnwire+chancloser: add new fee range TLV to co-op msg & use during negotiation #5644

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 65 additions & 16 deletions lnwallet/chancloser/chancloser.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ type ChanCloser struct {
// offer when starting negotiation. This will be used as a baseline.
idealFeeSat btcutil.Amount

// idealFeeSat stores min+max fee we're willing to accept when closing
// this channel. This can be used to compress the negotiation process
// down to 2 or 3 steps.
idealFeeRange lnwire.FeeRange

// lastFeeProposal is the last fee that we proposed to the remote party.
// We'll use this as a pivot point to ratchet our next offer up, down, or
// simply accept the remote party's prior offer.
Expand Down Expand Up @@ -186,18 +191,33 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
idealFeeSat = channelCommitFee
}

// Given this ideal fee, we'll then compute an acceptable range. For
// now, we'll treat this ideal fee rate as a floor, and accept a fee
// that's 20% larger
//
// TODO(roasbeef): how do we actually want to map this??
// * CalcFee actually over estimates as it assumes the base commit
// fee, co-op close smalle
// * only actually need to store this also?
// * instead use this as the max, and something equiv to 1 sat/byte
// as min?
idealFeeRange := lnwire.FeeRange{
MinFeeSats: cfg.Channel.CalcFee(chainfee.FeePerKwFloor),
MaxFeeSats: idealFeeSat,
}

chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) is: %v sat",
cfg.Channel.ChannelPoint(), int64(idealFeeSat))

cid := lnwire.NewChanIDFromOutPoint(cfg.Channel.ChannelPoint())
return &ChanCloser{
closeReq: closeReq,
state: closeIdle,
chanPoint: *cfg.Channel.ChannelPoint(),
cid: cid,
cfg: cfg,
negotiationHeight: negotiationHeight,
idealFeeSat: idealFeeSat,
closeReq: closeReq,
state: closeIdle,
chanPoint: *cfg.Channel.ChannelPoint(),
cid: cid,
cfg: cfg,
negotiationHeight: negotiationHeight, idealFeeSat: idealFeeSat,
idealFeeRange: idealFeeRange,
localDeliveryScript: deliveryScript,
priorFeeOffers: make(map[btcutil.Amount]*lnwire.ClosingSigned),
locallyInitiated: locallyInitiated,
Expand Down Expand Up @@ -492,8 +512,9 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
// We'll now attempt to ratchet towards a fee deemed acceptable by
// both parties, factoring in our ideal fee rate, and the last
// proposed fee by both sides.
feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
c.lastFeeProposal, remoteProposedFee,
feeProposal := calcCompromiseFee(
c.chanPoint, c.idealFeeSat, c.lastFeeProposal,
closeSignedMsg, &c.idealFeeRange,
)

// With our new fee proposal calculated, we'll craft a new close
Expand Down Expand Up @@ -614,10 +635,18 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign
chancloserLog.Infof("ChannelPoint(%v): proposing fee of %v sat to close "+
"chan", c.chanPoint, int64(fee))

// We'll assemble a ClosingSigned message using this information and return
// it to the caller so we can kick off the final stage of the channel
// closure process.
closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig)
// We'll assemble a ClosingSigned message using this information and
// return it to the caller so we can kick off the final stage of the
// channel closure process.
closeSignedMsg := lnwire.NewClosingSigned(
c.cid, fee, parsedSig, nil,
)

// If we're the initiator, then we'll add our fee range as well, so we
// can attempt to cut the process short.
if c.cfg.Channel.IsInitiator() {
closeSignedMsg.FeeRange = &c.idealFeeRange
}

// We'll also save this close signed, in the case that the remote party
// accepts our offer. This way, we don't have to re-sign.
Expand Down Expand Up @@ -661,19 +690,39 @@ func ratchetFee(fee btcutil.Amount, up bool) btcutil.Amount {
// calcCompromiseFee performs the current fee negotiation algorithm, taking
// into consideration our ideal fee based on current fee environment, the fee
// we last proposed (if any), and the fee proposed by the peer.
func calcCompromiseFee(chanPoint wire.OutPoint, ourIdealFee, lastSentFee,
remoteFee btcutil.Amount) btcutil.Amount {
func calcCompromiseFee(chanPoint wire.OutPoint,
ourIdealFee, lastSentFee btcutil.Amount,
remoteClose *lnwire.ClosingSigned,
ourFeeRange *lnwire.FeeRange) btcutil.Amount {

// TODO(roasbeef): take in number of rounds as well?
remoteFee := remoteClose.FeeSatoshis
remoteFeeRange := remoteClose.FeeRange != nil

chancloserLog.Infof("ChannelPoint(%v): computing fee compromise, ideal="+
"%v, last_sent=%v, remote_offer=%v", chanPoint, int64(ourIdealFee),
int64(lastSentFee), int64(remoteFee))

feeInRange := func(feeRange *lnwire.FeeRange, targetFee btcutil.Amount) bool {
return targetFee >= feeRange.MinFeeSats && targetFee <= ourFeeRange.MaxFeeSats
}

// Otherwise, we'll need to attempt to make a fee compromise if this is the
// second round, and neither side has agreed on fees.
switch {

// The remote party has sent us a fee range, so we'll check if our
// ideal fee rate is within their range, if so, then we'll just send
// over our ideal fee as they should accept it.
case remoteFeeRange && feeInRange(remoteClose.FeeRange, ourIdealFee):
return ourIdealFee

// If they didn't set the fee range (only the initiator should), then
// we'll just check to see if their fee is within our acceptable range,
// if so, we'll go along with it.
case feeInRange(ourFeeRange, remoteFee):
// TODO(roasbeef): or accept the max of the min fees?
return remoteFee

// If their proposed fee is identical to our ideal fee, then we'll go with
// that as we can short circuit the fee negotiation. Similarly, if we
// haven't sent an offer yet, we'll default to our ideal fee.
Expand Down
167 changes: 162 additions & 5 deletions lnwire/closing_signed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,141 @@ package lnwire

import (
"bytes"
"fmt"
"io"

"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/tlv"
)

const (
// ClosingSignedFeeRangeType is the type used to store the optional fee
// range field into the ClosingSigned message.
ClosingSignedFeeRangeType = 1

// FeeRangeRecordSize is the amount of bytes that fee range record size
// occupies (two 8 byte integers).
FeeRangeRecordSize = 16
)

// FeeRange is a structured TLV contained within the ClosingSigned message that
// allows both parties to opt into a more constrained negotiation algorithm. In
// the ideal case, the funder sends over their fee range, which is accepted by
// the responder and negotiation completes. The non-initiator is also able to
// initiate the new process by sending over a fee rate (with their max), with
// the funder echo'ing back the value if they find it to be acceptable.
type FeeRange struct {
// MinFeeSats is the smallest acceptable fee that the sending party will
// accept.
MinFeeSats btcutil.Amount

// MaxFeeSats is the largest acceptable fee that the sending party will
// accept.
MaxFeeSats btcutil.Amount
}

// NewRecord returns a TLV record that can be used to optionally encode a
// specified fee range to allow the fee negotiation process to be comprssed
// down to a minimal amount of steps.
func (f *FeeRange) NewRecord() tlv.Record {
return tlv.MakeStaticRecord(
ClosingSignedFeeRangeType, f, FeeRangeRecordSize,
eFeeRange, dFeeRange,
)
}

// eFeeRange is a function used to encode the fee range struct as a fixed sized
// TLV record.
func eFeeRange(w io.Writer, val interface{}, buf *[8]byte) error {
// Simply write out the stored as part of the fee range back to back.
if feeRange, ok := val.(*FeeRange); ok {
err := tlv.EUint64T(w, uint64(feeRange.MinFeeSats), buf)
if err != nil {
return err
}

return tlv.EUint64T(w, uint64(feeRange.MaxFeeSats), buf)
}
return tlv.NewTypeForEncodingErr(val, "FeeRange")
}

// dFeeRange attempts to decode the encoded TLV record within the passed
// io.Reader into the target val (FeeRange pointer).
func dFeeRange(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if feeRange, ok := val.(*FeeRange); ok && l == FeeRangeRecordSize {
var min, max uint64

if err := tlv.DUint64(r, &min, buf, 8); err != nil {
return err
}
if err := tlv.DUint64(r, &max, buf, 8); err != nil {
return err
}

feeRange.MinFeeSats = btcutil.Amount(min)
feeRange.MaxFeeSats = btcutil.Amount(max)
return nil
}
return tlv.NewTypeForDecodingErr(val, "FeeRange", l, FeeRangeRecordSize)
}

// packFeeRange attempts to encode a FeeRange TLV into the passed io.Writer if
// specified.
func packFeeRange(feeRange *FeeRange,
extraData ExtraOpaqueData) (ExtraOpaqueData, error) {

// We only need to encode this field if it's specified, so if it isn't
// present, then we'll just return the set of opaque bytes as is.
if feeRange == nil {
return extraData, nil
}

// Otherwise, we'll pack the feeRange directly in as a set of TLV
// records.
var tlvRecords ExtraOpaqueData
err := tlvRecords.PackRecords(feeRange.NewRecord())
if err != nil {
return nil, fmt.Errorf("unable to pack fee range as TLV "+
"record: %v", err)
}

return tlvRecords, nil
}

// parseFeeRange attempts to decode a FeeRange TLV along with any other TLV
// types that we may not recognize.
func parseFeeRange(tlvRecords ExtraOpaqueData) (
*FeeRange, ExtraOpaqueData, error) {

// If no TLV data is present there can't be any script available.
if len(tlvRecords) == 0 {
return nil, tlvRecords, nil
}

// We have some TLV data, so we'll attempt to parse out the optional
// fee range record.
feeRange := new(FeeRange)
parsedTLVs, err := tlvRecords.ExtractRecords(feeRange.NewRecord())
if err != nil {
return nil, nil, err
}

// If the set of parsed TLVs doesn't include the optional fee range
// type, then we'll just return a nil value, and the opaque data as is.
_, ok := parsedTLVs[ClosingSignedFeeRangeType]
if !ok {
return nil, tlvRecords, nil
}

// Otherwise, we've found the type we're looking for (has been decoded
// into the value), so we'll return it directly, snipping off the bytes
// of the extra data.
//
// TODO(roasbeef): bring back old method by wpaulino that does this?
tlvRecords = tlvRecords[FeeRangeRecordSize+2:]
return feeRange, tlvRecords, nil
}

// ClosingSigned is sent by both parties to a channel once the channel is clear
// of HTLCs, and is primarily concerned with negotiating fees for the close
// transaction. Each party provides a signature for a transaction with a fee
Expand All @@ -29,19 +159,24 @@ type ClosingSigned struct {
// Signature is for the proposed channel close transaction.
Signature Sig

// FeeRange is an optional set of fee range hints both sides can
// provide in order to compress the co-op close negotiation process.
FeeRange *FeeRange

// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
ExtraData ExtraOpaqueData
}

// NewClosingSigned creates a new empty ClosingSigned message.
func NewClosingSigned(cid ChannelID, fs btcutil.Amount,
sig Sig) *ClosingSigned {
func NewClosingSigned(cid ChannelID, fs btcutil.Amount, sig Sig,
fr *FeeRange) *ClosingSigned {

return &ClosingSigned{
ChannelID: cid,
FeeSatoshis: fs,
FeeRange: fr,
Signature: sig,
}
}
Expand All @@ -55,16 +190,38 @@ var _ Message = (*ClosingSigned)(nil)
//
// This is part of the lnwire.Message interface.
func (c *ClosingSigned) Decode(r io.Reader, pver uint32) error {
return ReadElements(
r, &c.ChannelID, &c.FeeSatoshis, &c.Signature, &c.ExtraData,
err := ReadElements(
r, &c.ChannelID, &c.FeeSatoshis, &c.Signature,
)
if err != nil {
return err
}

var tlvRecords ExtraOpaqueData
if err := ReadElements(r, &tlvRecords); err != nil {
return err
}

c.FeeRange, c.ExtraData, err = parseFeeRange(
tlvRecords,
)
if err != nil {
return err
}

return nil
}

// Encode serializes the target ClosingSigned into the passed io.Writer
// observing the protocol version specified.
//
// This is part of the lnwire.Message interface.
func (c *ClosingSigned) Encode(w *bytes.Buffer, pver uint32) error {
tlvRecords, err := packFeeRange(c.FeeRange, c.ExtraData)
if err != nil {
return err
}

if err := WriteChannelID(w, c.ChannelID); err != nil {
return err
}
Expand All @@ -77,7 +234,7 @@ func (c *ClosingSigned) Encode(w *bytes.Buffer, pver uint32) error {
return err
}

return WriteBytes(w, c.ExtraData)
return WriteBytes(w, tlvRecords)
}

// MsgType returns the integer uniquely identifying this message type on the
Expand Down
9 changes: 9 additions & 0 deletions lnwire/lnwire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,15 @@ func TestLightningWireProtocol(t *testing.T) {
return
}

// With a 50/50 chance, add in a TLV record with the
// FeeRange field specified.
if r.Intn(2) == 0 {
req.FeeRange = &FeeRange{
MinFeeSats: btcutil.Amount(uint64(r.Int63())),
MaxFeeSats: btcutil.Amount(uint64(r.Int63())),
}
}

v[0] = reflect.ValueOf(req)
},
MsgCommitSig: func(v []reflect.Value, r *rand.Rand) {
Expand Down