Skip to content

Commit 16e024d

Browse files
committed
chancloser: add FeeRange logic
1 parent 4a18d3f commit 16e024d

File tree

1 file changed

+229
-24
lines changed

1 file changed

+229
-24
lines changed

lnwallet/chancloser/chancloser.go

+229-24
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ var (
4343
// ErrInvalidShutdownScript is returned when we receive an address from
4444
// a peer that isn't either a p2wsh or p2tr address.
4545
ErrInvalidShutdownScript = fmt.Errorf("invalid shutdown script")
46+
47+
// ErrCloseTypeChanged is returned when the counterparty attempts to
48+
// change the negotiation type from fee-range to legacy or vice versa.
49+
ErrCloseTypeChanged = fmt.Errorf("close type changed")
50+
51+
// ErrNoRangeOverlap is returned when there is no overlap between our
52+
// and our counterparty's fee_range.
53+
ErrNoRangeOverlap = fmt.Errorf("no range overlap")
54+
55+
// ErrFeeNotInOverlap is returned when the counterparty sends a fee that
56+
// is not in the overlapping fee_range.
57+
ErrFeeNotInOverlap = fmt.Errorf("fee not in overlap")
58+
59+
// ErrFeeRangeViolation is returned when the fundee receives a bad
60+
// FeeRange from the funder after the fundee has sent their one and
61+
// only FeeRange to the funder.
62+
ErrFeeRangeViolation = fmt.Errorf("fee range violation")
4663
)
4764

4865
// closeState represents all the possible states the channel closer state
@@ -219,6 +236,9 @@ type ChanCloser struct {
219236
// idealFeeRate is our ideal fee rate.
220237
idealFeeRate chainfee.SatPerKWeight
221238

239+
// idealFeeRange is our ideal fee range.
240+
idealFeeRange *lnwire.FeeRange
241+
222242
// lastFeeProposal is the last fee that we proposed to the remote party.
223243
// We'll use this as a pivot point to ratchet our next offer up, down, or
224244
// simply accept the remote party's prior offer.
@@ -271,6 +291,16 @@ type ChanCloser struct {
271291
// waiting for the peer's Shutdown and will call ChannelClean when we
272292
// receive it. This is only used when we restart the connection.
273293
cleanOnRecv bool
294+
295+
// legacyNegotiation means that legacy negotiation has been initiated.
296+
// This is used so that the remote can't change from legacy to range
297+
// based negotiation.
298+
legacyNegotiation bool
299+
300+
// rangeNegotiation means that range-based negotiation has been
301+
// initiated. This is used so the remote can't change from range-based
302+
// to legacy negotiation.
303+
rangeNegotiation bool
274304
}
275305

276306
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
@@ -371,6 +401,24 @@ func (c *ChanCloser) initFeeBaseline() {
371401
)
372402
}
373403

404+
// Calculate the minimum fee we'll accept for the fee range.
405+
minFeeSats := c.cfg.FeeEstimator.EstimateFee(
406+
0, localTxOut, remoteTxOut, chainfee.FeePerKwFloor,
407+
)
408+
409+
// Populate the fee range. If minFeeSats is greater than idealFeeSat,
410+
// use idealFeeSat as the minimum. This may happen since FeePerKwFloor
411+
// uses 253 sat/kw instead of 250 sat/kw.
412+
c.idealFeeRange = &lnwire.FeeRange{
413+
MaxFeeSats: c.maxFee,
414+
}
415+
416+
if minFeeSats > c.idealFeeSat {
417+
c.idealFeeRange.MinFeeSats = c.idealFeeSat
418+
} else {
419+
c.idealFeeRange.MinFeeSats = minFeeSats
420+
}
421+
374422
chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
375423
"is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
376424
int64(c.idealFeeSat), int64(c.maxFee))
@@ -695,34 +743,20 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message, remote bool) (
695743
// we'll attempt to ratchet the fee closer to
696744
remoteProposedFee := closeSignedMsg.FeeSatoshis
697745
if _, ok := c.priorFeeOffers[remoteProposedFee]; !ok {
698-
// We'll now attempt to ratchet towards a fee deemed acceptable by
699-
// both parties, factoring in our ideal fee rate, and the last
700-
// proposed fee by both sides.
701-
feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
702-
c.lastFeeProposal, remoteProposedFee,
703-
)
704-
if c.cfg.Channel.IsInitiator() && feeProposal > c.maxFee {
705-
return nil, false, fmt.Errorf("%w: %v > %v",
706-
ErrProposalExeceedsMaxFee, feeProposal,
707-
c.maxFee)
708-
}
709-
710-
// With our new fee proposal calculated, we'll craft a new close
711-
// signed signature to send to the other party so we can continue
712-
// the fee negotiation process.
713-
closeSigned, err := c.proposeCloseSigned(feeProposal)
746+
response, err := c.handleRemoteProposal(closeSignedMsg)
714747
if err != nil {
748+
// If an error was returned, no message was
749+
// returned. Bubble up the error.
715750
return nil, false, err
716751
}
717752

718-
// If the compromise fee doesn't match what the peer proposed, then
719-
// we'll return this latest close signed message so we can continue
720-
// negotiation.
721-
if feeProposal != remoteProposedFee {
722-
chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+
723-
"disagreement, continuing negotiation", c.chanPoint)
724-
return []lnwire.Message{closeSigned}, false, nil
753+
// If a response was returned, bubble up the response.
754+
if response != nil {
755+
return []lnwire.Message{response}, false, nil
725756
}
757+
758+
// Else, no response or error was returned so we can
759+
// finish up negotiation.
726760
}
727761

728762
chancloserLog.Infof("ChannelPoint(%v) fee of %v accepted, ending "+
@@ -828,7 +862,9 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign
828862
// We'll assemble a ClosingSigned message using this information and return
829863
// it to the caller so we can kick off the final stage of the channel
830864
// closure process.
831-
closeSignedMsg := lnwire.NewClosingSigned(c.cid, fee, parsedSig, nil)
865+
closeSignedMsg := lnwire.NewClosingSigned(
866+
c.cid, fee, parsedSig, c.idealFeeRange,
867+
)
832868

833869
// We'll also save this close signed, in the case that the remote party
834870
// accepts our offer. This way, we don't have to re-sign.
@@ -837,6 +873,175 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (*lnwire.ClosingSign
837873
return closeSignedMsg, nil
838874
}
839875

876+
// handleRemoteProposal sanity checks the remote's ClosingSigned message and
877+
// tries to determine an acceptable fee to reply with. It may return:
878+
// - a message and no error (continue negotiation)
879+
// - no message and an error (negotiation failed)
880+
// - no message and no error (indicating we agree with the remote's fee)
881+
func (c *ChanCloser) handleRemoteProposal(
882+
remoteMsg *lnwire.ClosingSigned) (lnwire.Message, error) {
883+
884+
isFunder := c.cfg.Channel.IsInitiator()
885+
886+
// If FeeRange is set, perform FeeRange-specific checks.
887+
if remoteMsg.FeeRange != nil {
888+
// If legacyNegotiation is already set, fail outright.
889+
if c.legacyNegotiation {
890+
return nil, ErrCloseTypeChanged
891+
}
892+
893+
// Set rangeNegotiation to true.
894+
c.rangeNegotiation = true
895+
896+
// Get the intersection of our two FeeRanges if one exists.
897+
overlap := c.idealFeeRange.GetOverlap(remoteMsg.FeeRange)
898+
899+
if isFunder {
900+
// If the fundee replies with a FeeRange, there must be
901+
// overlap with our FeeRange.
902+
//
903+
// BOLT#02:
904+
// - otherwise (it is not the funder)
905+
// - ...
906+
// - otherwise
907+
// - MUST propose a fee_satoshis in the overlap
908+
// between received and (about-to-be) sent
909+
// fee_range.
910+
if overlap == nil {
911+
return nil, ErrNoRangeOverlap
912+
}
913+
914+
// This is included in the above requirement.
915+
if !overlap.InRange(remoteMsg.FeeSatoshis) {
916+
return nil, ErrFeeNotInOverlap
917+
}
918+
919+
// If the above checks pass, the funder must reply with
920+
// the same fee_satoshis.
921+
//
922+
// BOLT#02:
923+
// - if it is the funder:
924+
// - ...
925+
// - otherwise:
926+
// - MUST reply with the same fee_satoshis.
927+
_, err := c.proposeCloseSigned(remoteMsg.FeeSatoshis)
928+
if err != nil {
929+
return nil, err
930+
}
931+
932+
// Return nil values to indicate we are done with
933+
// negotiation.
934+
return nil, nil
935+
}
936+
937+
// If we are the fundee and we have already sent a ClosingSigned
938+
// to the funder, we should not be calling this function.
939+
//
940+
// BOLT#02:
941+
// - if it is the funder:
942+
// - ...
943+
// - otherwise:
944+
// - MUST reply with the same fee_satoshis.
945+
//
946+
// Since the funder must reply with the same fee_satoshis, the
947+
// calling function should not call into this negotiation
948+
// function.
949+
if c.lastFeeProposal != 0 {
950+
return nil, ErrFeeRangeViolation
951+
}
952+
953+
// If we are the fundee and there is no overlap between their
954+
// fee_range and our yet-to-be-sent fee_range, send a warning.
955+
//
956+
// BOLT#02:
957+
// - if there is no overlap between that and its own fee_range
958+
// - SHOULD send a warning.
959+
//
960+
// NOTE: The above SHOULD will probably be changed to MUST.
961+
if overlap == nil {
962+
warning := lnwire.NewWarning()
963+
warning.ChanID = remoteMsg.ChannelID
964+
warning.Data = lnwire.WarningData("ClosingSigned: no " +
965+
"fee_range overlap")
966+
return warning, nil
967+
}
968+
969+
// If we've reached this point, then we have to propose a fee
970+
// in the overlap.
971+
//
972+
// BOLT#02:
973+
// - otherwise (it is not the funder)
974+
// - ...
975+
// - otherwise
976+
// - MUST propose a fee_satoshis in the overlap between
977+
// received and (about-to-be) sent fee_range.
978+
//
979+
// If our ideal fee is in the overlap, use that. If it's not in
980+
// the overlap, use the upper bound of the overlap.
981+
var feeProposal btcutil.Amount
982+
if overlap.InRange(c.idealFeeSat) {
983+
feeProposal = c.idealFeeSat
984+
} else {
985+
feeProposal = overlap.MaxFeeSats
986+
}
987+
988+
closeSigned, err := c.proposeCloseSigned(feeProposal)
989+
if err != nil {
990+
return nil, err
991+
}
992+
993+
// If the feeProposal is not equal to the remote's FeeSatoshis,
994+
// negotiation isn't done.
995+
if feeProposal != remoteMsg.FeeSatoshis {
996+
return closeSigned, nil
997+
}
998+
999+
// Otherwise, negotiation is done.
1000+
return nil, nil
1001+
}
1002+
1003+
// Else, do the legacy negotiation. If rangeNegotiation is already set,
1004+
// fail outright.
1005+
if c.rangeNegotiation {
1006+
return nil, ErrCloseTypeChanged
1007+
}
1008+
1009+
// Set legacyNegotiation to true.
1010+
c.legacyNegotiation = true
1011+
1012+
// We'll now attempt to ratchet towards a fee deemed acceptable by both
1013+
// parties, factoring in our ideal fee rate, and the last proposed fee
1014+
// by both sides.
1015+
feeProposal := calcCompromiseFee(
1016+
c.chanPoint, c.idealFeeSat, c.lastFeeProposal,
1017+
remoteMsg.FeeSatoshis,
1018+
)
1019+
if isFunder && feeProposal > c.maxFee {
1020+
return nil, fmt.Errorf("%w: %v > %v", ErrProposalExeceedsMaxFee,
1021+
feeProposal, c.maxFee)
1022+
}
1023+
1024+
// With our new fee proposal calculated, we'll craft a new close signed
1025+
// signature to send to the other party so we can continue the fee
1026+
// negotiation process.
1027+
closeSigned, err := c.proposeCloseSigned(feeProposal)
1028+
if err != nil {
1029+
return nil, err
1030+
}
1031+
1032+
// If the compromise fee doesn't match what the peer proposed, then
1033+
// we'll return this latest close signed message so we can continue
1034+
// negotiation.
1035+
if feeProposal != remoteMsg.FeeSatoshis {
1036+
chancloserLog.Debugf("ChannelPoint(%v): close tx fee "+
1037+
"disagreement, continuing negotiation", c.chanPoint)
1038+
return closeSigned, nil
1039+
}
1040+
1041+
// We are done with negotiation.
1042+
return nil, nil
1043+
}
1044+
8401045
// feeInAcceptableRange returns true if the passed remote fee is deemed to be
8411046
// in an "acceptable" range to our local fee. This is an attempt at a
8421047
// compromise and to ensure that the fee negotiation has a stopping point. We

0 commit comments

Comments
 (0)