diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go index 0fe643bcf8..4ad0c8ce44 100644 --- a/lnwallet/chancloser/chancloser.go +++ b/lnwallet/chancloser/chancloser.go @@ -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. @@ -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, @@ -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 @@ -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. @@ -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. diff --git a/lnwire/closing_signed.go b/lnwire/closing_signed.go index 9ba170ba5e..39deeb1810 100644 --- a/lnwire/closing_signed.go +++ b/lnwire/closing_signed.go @@ -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 @@ -29,6 +159,10 @@ 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. @@ -36,12 +170,13 @@ type ClosingSigned struct { } // 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, } } @@ -55,9 +190,26 @@ 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 @@ -65,6 +217,11 @@ func (c *ClosingSigned) Decode(r io.Reader, pver uint32) error { // // 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 } @@ -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 diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go index 4475b33825..bc2c983a01 100644 --- a/lnwire/lnwire_test.go +++ b/lnwire/lnwire_test.go @@ -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) {