diff --git a/funding/commitment_type_negotiation.go b/funding/commitment_type_negotiation.go index e66dc78b2..091c6fcbb 100644 --- a/funding/commitment_type_negotiation.go +++ b/funding/commitment_type_negotiation.go @@ -50,6 +50,23 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, channelFeatures := lnwire.RawFeatureVector(channelType) switch { + // Lease script enforcement + anchors zero fee + static remote key + // features only. + case channelFeatures.OnlyContains( + lnwire.ScriptEnforcedLeaseRequired, + lnwire.AnchorsZeroFeeHtlcTxRequired, + lnwire.StaticRemoteKeyRequired, + ): + if !hasFeatures( + local, remote, + lnwire.ScriptEnforcedLeaseOptional, + lnwire.AnchorsZeroFeeHtlcTxOptional, + lnwire.StaticRemoteKeyOptional, + ) { + return 0, errUnsupportedChannelType + } + return lnwallet.CommitmentTypeScriptEnforcedLease, nil + // Anchors zero fee + static remote key features only. case channelFeatures.OnlyContains( lnwire.AnchorsZeroFeeHtlcTxRequired, diff --git a/funding/manager.go b/funding/manager.go index 7479d5082..b4fad5570 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -1364,6 +1364,28 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, } reservation.SetOurUpfrontShutdown(shutdown) + // If a script enforced channel lease is being proposed, we'll need to + // validate its custom TLV records. + if commitType == lnwallet.CommitmentTypeScriptEnforcedLease { + if msg.LeaseExpiry == nil { + err := errors.New("missing lease expiry") + f.failFundingFlow(peer, msg.PendingChannelID, err) + return + } + + // If we had a shim registered for this channel prior to + // receiving its corresponding OpenChannel message, then we'll + // validate the proposed LeaseExpiry against what was registered + // in our shim. + if reservation.LeaseExpiry() != 0 { + if uint32(*msg.LeaseExpiry) != reservation.LeaseExpiry() { + err := errors.New("lease expiry mismatch") + f.failFundingFlow(peer, msg.PendingChannelID, err) + return + } + } + } + log.Infof("Requiring %v confirmations for pendingChan(%x): "+ "amt=%v, push_amt=%v, committype=%v, upfrontShutdown=%x", numConfsReq, msg.PendingChannelID, amt, msg.PushAmount, @@ -1499,6 +1521,7 @@ func (f *Manager) handleFundingOpen(peer lnpeer.Peer, FirstCommitmentPoint: ourContribution.FirstCommitmentPoint, UpfrontShutdownScript: ourContribution.UpfrontShutdown, ChannelType: msg.ChannelType, + LeaseExpiry: msg.LeaseExpiry, } if err := peer.SendMessage(true, &fundingAccept); err != nil { @@ -1530,11 +1553,12 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer, log.Infof("Recv'd fundingResponse for pending_id(%x)", pendingChanID[:]) - // We'll want to quickly check that ChannelType echoed by the channel - // request recipient matches what we proposed. + // Perform some basic validation of any custom TLV records included. // // TODO: Return errors as funding.Error to give context to remote peer? if resCtx.channelType != nil { + // We'll want to quickly check that the ChannelType echoed by + // the channel request recipient matches what we proposed. if msg.ChannelType == nil { err := errors.New("explicit channel type not echoed back") f.failFundingFlow(peer, msg.PendingChannelID, err) @@ -1547,6 +1571,21 @@ func (f *Manager) handleFundingAccept(peer lnpeer.Peer, f.failFundingFlow(peer, msg.PendingChannelID, err) return } + + // We'll want to do the same with the LeaseExpiry if one should + // be set. + if resCtx.reservation.LeaseExpiry() != 0 { + if msg.LeaseExpiry == nil { + err := errors.New("lease expiry not echoed back") + f.failFundingFlow(peer, msg.PendingChannelID, err) + return + } + if uint32(*msg.LeaseExpiry) != resCtx.reservation.LeaseExpiry() { + err := errors.New("lease expiry mismatch") + f.failFundingFlow(peer, msg.PendingChannelID, err) + return + } + } } else if msg.ChannelType != nil { err := errors.New("received unexpected channel type") f.failFundingFlow(peer, msg.PendingChannelID, err) @@ -3206,9 +3245,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { // For anchor channels cap the initial commit fee rate at our defined // maximum. - if commitType == lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx && + if commitType.HasAnchors() && commitFeePerKw > f.cfg.MaxAnchorsCommitFeeRate { - commitFeePerKw = f.cfg.MaxAnchorsCommitFeeRate } @@ -3311,6 +3349,14 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { // remote party. chanReserve := f.cfg.RequiredRemoteChanReserve(capacity, ourDustLimit) + // When opening a script enforced channel lease, include the required + // expiry TLV record in our proposal. + var leaseExpiry *lnwire.LeaseExpiry + if commitType == lnwallet.CommitmentTypeScriptEnforcedLease { + leaseExpiry = new(lnwire.LeaseExpiry) + *leaseExpiry = lnwire.LeaseExpiry(reservation.LeaseExpiry()) + } + log.Infof("Starting funding workflow with %v for pending_id(%x), "+ "committype=%v", msg.Peer.Address(), chanID, commitType) @@ -3335,6 +3381,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) { ChannelFlags: channelFlags, UpfrontShutdownScript: shutdown, ChannelType: msg.ChannelType, + LeaseExpiry: leaseExpiry, } if err := msg.Peer.SendMessage(true, &fundingOpen); err != nil { e := fmt.Errorf("unable to send funding request message: %v", diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index 00519f823..c5e7dfc34 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -1,6 +1,7 @@ package lnwallet import ( + "errors" "net" "sync" @@ -34,8 +35,40 @@ const ( // requires second-level HTLC transactions to be signed using a // zero-fee. CommitmentTypeAnchorsZeroFeeHtlcTx + + // CommitmentTypeScriptEnforcedLease is a commitment type that builds + // upon CommitmentTypeTweakless and CommitmentTypeAnchorsZeroFeeHtlcTx, + // which in addition requires a CLTV clause to spend outputs paying to + // the channel initiator. This is intended for use on leased channels to + // guarantee that the channel initiator has no incentives to close a + // leased channel before its maturity date. + CommitmentTypeScriptEnforcedLease ) +// HasStaticRemoteKey returns whether the commitment type supports remote +// outputs backed by static keys. +func (c CommitmentType) HasStaticRemoteKey() bool { + switch c { + case CommitmentTypeTweakless, + CommitmentTypeAnchorsZeroFeeHtlcTx, + CommitmentTypeScriptEnforcedLease: + return true + default: + return false + } +} + +// HasAnchors returns whether the commitment type supports anchor outputs. +func (c CommitmentType) HasAnchors() bool { + switch c { + case CommitmentTypeAnchorsZeroFeeHtlcTx, + CommitmentTypeScriptEnforcedLease: + return true + default: + return false + } +} + // String returns the name of the CommitmentType. func (c CommitmentType) String() string { switch c { @@ -45,6 +78,8 @@ func (c CommitmentType) String() string { return "tweakless" case CommitmentTypeAnchorsZeroFeeHtlcTx: return "anchors-zero-fee-second-level" + case CommitmentTypeScriptEnforcedLease: + return "script-enforced-lease" default: return "invalid" } @@ -188,8 +223,8 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // Based on the channel type, we determine the initial commit weight // and fee. commitWeight := int64(input.CommitWeight) - if commitType == CommitmentTypeAnchorsZeroFeeHtlcTx { - commitWeight = input.AnchorCommitWeight + if commitType.HasAnchors() { + commitWeight = int64(input.AnchorCommitWeight) } commitFee := commitFeePerKw.FeeForWeight(commitWeight) @@ -201,7 +236,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // The total fee paid by the initiator will be the commitment fee in // addition to the two anchor outputs. feeMSat := lnwire.NewMSatFromSatoshis(commitFee) - if commitType == CommitmentTypeAnchorsZeroFeeHtlcTx { + if commitType.HasAnchors() { feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize) } @@ -288,8 +323,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, if ourBalance == 0 || theirBalance == 0 || pushMSat != 0 { // Both the tweakless type and the anchor type is tweakless, // hence set the bit. - if commitType == CommitmentTypeTweakless || - commitType == CommitmentTypeAnchorsZeroFeeHtlcTx { + if commitType.HasStaticRemoteKey() { chanType |= channeldb.SingleFunderTweaklessBit } else { chanType |= channeldb.SingleFunderBit @@ -325,14 +359,20 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, // We are adding anchor outputs to our commitment. We only support this // in combination with zero-fee second-levels HTLCs. - if commitType == CommitmentTypeAnchorsZeroFeeHtlcTx { + if commitType.HasAnchors() { chanType |= channeldb.AnchorOutputsBit chanType |= channeldb.ZeroHtlcTxFeeBit } - // If the channel is meant to be frozen, then we'll set the frozen bit - // now so once the channel is open, it can be interpreted properly. - if thawHeight != 0 { + // Set the appropriate LeaseExpiration/Frozen bit based on the + // reservation parameters. + if commitType == CommitmentTypeScriptEnforcedLease { + if thawHeight == 0 { + return nil, errors.New("missing absolute expiration " + + "for script enforced lease commitment type") + } + chanType |= channeldb.LeaseExpirationBit + } else if thawHeight > 0 { chanType |= channeldb.FrozenBit } @@ -719,6 +759,16 @@ func (r *ChannelReservation) Capacity() btcutil.Amount { return r.partialState.Capacity } +// LeaseExpiry returns the absolute expiration height for a leased channel using +// the script enforced commitment type. A zero value is returned when the +// channel is not using a script enforced lease commitment type. +func (r *ChannelReservation) LeaseExpiry() uint32 { + if !r.partialState.ChanType.HasLeaseExpiration() { + return 0 + } + return r.partialState.ThawHeight +} + // Cancel abandons this channel reservation. This method should be called in // the scenario that communications with the counterparty break down. Upon // cancellation, all resources previously reserved for this pending payment diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 024146530..32d094053 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -818,7 +818,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg // funding tx ready, so this will always pass. We'll do another check // when the PSBT has been verified. isPublic := req.Flags&lnwire.FFAnnounceChannel != 0 - hasAnchors := req.CommitType == CommitmentTypeAnchorsZeroFeeHtlcTx + hasAnchors := req.CommitType.HasAnchors() err = l.enforceNewReservedValue(fundingIntent, isPublic, hasAnchors) if err != nil { fundingIntent.Cancel()