diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go index 61e819391..b402285be 100644 --- a/contractcourt/breacharbiter_test.go +++ b/contractcourt/breacharbiter_test.go @@ -2299,6 +2299,7 @@ func createInitChannels(revocationWindow int) (*lnwallet.LightningChannel, *lnwa aliceCommitTx, bobCommitTx, err := lnwallet.CreateCommitmentTxns( channelBal, channelBal, &aliceCfg, &bobCfg, aliceCommitPoint, bobCommitPoint, *fundingTxIn, channeldb.SingleFunderTweaklessBit, + false, 0, ) if err != nil { return nil, nil, nil, err diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 2bf8990eb..973a0ca73 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -362,8 +362,13 @@ func (c *chainWatcher) handleUnknownLocalState( // With the keys derived, we'll construct the remote script that'll be // present if they have a non-dust balance on the commitment. + var leaseExpiry uint32 + if c.cfg.chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = c.cfg.chanState.ThawHeight + } remoteScript, _, err := lnwallet.CommitScriptToRemote( - c.cfg.chanState.ChanType, commitKeyRing.ToRemoteKey, + c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, + commitKeyRing.ToRemoteKey, leaseExpiry, ) if err != nil { return false, err @@ -373,8 +378,9 @@ func (c *chainWatcher) handleUnknownLocalState( // the remote party allowing them to claim this output before the CSV // delay if we breach. localScript, err := lnwallet.CommitScriptToSelf( + c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey, - uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), + uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry, ) if err != nil { return false, err diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index eaf2aa99c..3fa57f601 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -134,6 +134,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, channelCapacity := aliceAmount + bobAmount csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) + isAliceInitiator := true aliceConstraints := &channeldb.ChannelConstraints{ DustLimit: btcutil.Amount(200), @@ -230,6 +231,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, aliceCommitTx, bobCommitTx, err := lnwallet.CreateCommitmentTxns( aliceAmount, bobAmount, &aliceCfg, &bobCfg, aliceCommitPoint, bobCommitPoint, *fundingTxIn, channeldb.SingleFunderTweaklessBit, + isAliceInitiator, 0, ) if err != nil { return nil, nil, nil, err @@ -298,7 +300,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, IdentityPub: aliceKeyPub, FundingOutpoint: *prevOut, ChanType: channeldb.SingleFunderTweaklessBit, - IsInitiator: true, + IsInitiator: isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: bobCommitPoint, RevocationProducer: alicePreimageProducer, @@ -317,7 +319,7 @@ func createTestChannel(alicePrivKey, bobPrivKey []byte, IdentityPub: bobKeyPub, FundingOutpoint: *prevOut, ChanType: channeldb.SingleFunderTweaklessBit, - IsInitiator: false, + IsInitiator: !isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: aliceCommitPoint, RevocationProducer: bobPreimageProducer, diff --git a/input/size_test.go b/input/size_test.go index 8008ac854..b2d1e2bee 100644 --- a/input/size_test.go +++ b/input/size_test.go @@ -861,8 +861,8 @@ func TestWitnessSizes(t *testing.T) { func genTimeoutTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { // Create the unsigned timeout tx. timeoutTx, err := lnwallet.CreateHtlcTimeoutTx( - chanType, testOutPoint, testAmt, testCLTVExpiry, - testCSVDelay, testPubkey, testPubkey, + chanType, false, testOutPoint, testAmt, testCLTVExpiry, + testCSVDelay, 0, testPubkey, testPubkey, ) if err != nil { return nil, err @@ -903,7 +903,7 @@ func genTimeoutTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { func genSuccessTx(chanType channeldb.ChannelType) (*wire.MsgTx, error) { // Create the unisgned success tx. successTx, err := lnwallet.CreateHtlcSuccessTx( - chanType, testOutPoint, testAmt, testCSVDelay, + chanType, false, testOutPoint, testAmt, testCSVDelay, 0, testPubkey, testPubkey, ) if err != nil { diff --git a/lnwallet/channel.go b/lnwallet/channel.go index e448d3863..917f06b1d 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2315,8 +2315,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // number so we can have the proper witness script to sign and include // within the final witness. theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) + isRemoteInitiator := !chanState.IsInitiator + var leaseExpiry uint32 + if chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = chanState.ThawHeight + } theirScript, err := CommitScriptToSelf( - keyRing.ToLocalKey, keyRing.RevocationKey, theirDelay, + chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, + keyRing.RevocationKey, theirDelay, leaseExpiry, ) if err != nil { return nil, err @@ -2325,7 +2331,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Since it is the remote breach we are reconstructing, the output going // to us will be a to-remote script with our local params. ourScript, ourDelay, err := CommitScriptToRemote( - chanState.ChanType, keyRing.ToRemoteKey, + chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, + leaseExpiry, ) if err != nil { return nil, err @@ -2410,7 +2417,9 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // remote commitment transaction, and *they* go to the second // level. secondLevelScript, err := SecondLevelHtlcScript( + chanState.ChanType, isRemoteInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay, + leaseExpiry, ) if err != nil { return nil, err @@ -2968,8 +2977,8 @@ func processFeeUpdate(feeUpdate *PaymentDescriptor, nextHeight uint64, // signature can be submitted to the sigPool to generate all the signatures // asynchronously and in parallel. func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, - chanType channeldb.ChannelType, - localChanCfg, remoteChanCfg *channeldb.ChannelConfig, + chanType channeldb.ChannelType, isRemoteInitiator bool, + leaseExpiry uint32, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, remoteCommitView *commitment) ([]SignJob, chan struct{}, error) { txHash := remoteCommitView.txn.TxHash() @@ -3019,9 +3028,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = CreateHtlcTimeoutTx( - chanType, op, outputAmt, htlc.Timeout, - uint32(remoteChanCfg.CsvDelay), - keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, isRemoteInitiator, op, outputAmt, + htlc.Timeout, uint32(remoteChanCfg.CsvDelay), + leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { return nil, nil, err @@ -3072,7 +3081,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, Index: uint32(htlc.remoteOutputIndex), } sigJob.Tx, err = CreateHtlcSuccessTx( - chanType, op, outputAmt, uint32(remoteChanCfg.CsvDelay), + chanType, isRemoteInitiator, op, outputAmt, + uint32(remoteChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -3581,10 +3591,14 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, []ch // need to generate signatures of each of them for the remote party's // commitment state. We do so in two phases: first we generate and // submit the set of signature jobs to the worker pool. + var leaseExpiry uint32 + if lc.channelState.ChanType.HasLeaseExpiration() { + leaseExpiry = lc.channelState.ThawHeight + } sigBatch, cancelChan, err := genRemoteHtlcSigJobs( - keyRing, lc.channelState.ChanType, - &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, - newCommitView, + keyRing, lc.channelState.ChanType, !lc.channelState.IsInitiator, + leaseExpiry, &lc.channelState.LocalChanCfg, + &lc.channelState.RemoteChanCfg, newCommitView, ) if err != nil { return sig, htlcSigs, nil, err @@ -4067,7 +4081,7 @@ func (lc *LightningChannel) computeView(view *htlcView, remoteChain bool, // directly into the pool of workers. func genHtlcSigValidationJobs(localCommitmentView *commitment, keyRing *CommitmentKeyRing, htlcSigs []lnwire.Sig, - chanType channeldb.ChannelType, + chanType channeldb.ChannelType, isLocalInitiator bool, leaseExpiry uint32, localChanCfg, remoteChanCfg *channeldb.ChannelConfig) ([]VerifyJob, error) { txHash := localCommitmentView.txn.TxHash() @@ -4116,9 +4130,10 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, outputAmt := htlc.Amount.ToSatoshis() - htlcFee successTx, err := CreateHtlcSuccessTx( - chanType, op, outputAmt, - uint32(localChanCfg.CsvDelay), - keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, isLocalInitiator, op, + outputAmt, uint32(localChanCfg.CsvDelay), + leaseExpiry, keyRing.RevocationKey, + keyRing.ToLocalKey, ) if err != nil { return nil, err @@ -4170,8 +4185,9 @@ func genHtlcSigValidationJobs(localCommitmentView *commitment, outputAmt := htlc.Amount.ToSatoshis() - htlcFee timeoutTx, err := CreateHtlcTimeoutTx( - chanType, op, outputAmt, htlc.Timeout, - uint32(localChanCfg.CsvDelay), + chanType, isLocalInitiator, op, + outputAmt, htlc.Timeout, + uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { @@ -4385,9 +4401,14 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig, // As an optimization, we'll generate a series of jobs for the worker // pool to verify each of the HTLc signatures presented. Once // generated, we'll submit these jobs to the worker pool. + var leaseExpiry uint32 + if lc.channelState.ChanType.HasLeaseExpiration() { + leaseExpiry = lc.channelState.ThawHeight + } verifyJobs, err := genHtlcSigValidationJobs( localCommitmentView, keyRing, htlcSigs, - lc.channelState.ChanType, &lc.channelState.LocalChanCfg, + lc.channelState.ChanType, lc.channelState.IsInitiator, + leaseExpiry, &lc.channelState.LocalChanCfg, &lc.channelState.RemoteChanCfg, ) if err != nil { @@ -5570,18 +5591,24 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si // First, we'll generate the commitment point and the revocation point // so we can re-construct the HTLC state and also our payment key. + isOurCommit := false keyRing := DeriveCommitmentKeys( - commitPoint, false, chanState.ChanType, + commitPoint, isOurCommit, chanState.ChanType, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) // Next, we'll obtain HTLC resolutions for all the outgoing HTLC's we // had on their commitment transaction. + var leaseExpiry uint32 + if chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = chanState.ThawHeight + } + isRemoteInitiator := !chanState.IsInitiator htlcResolutions, err := extractHtlcResolutions( - chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer, - remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, + chainfee.SatPerKWeight(remoteCommit.FeePerKw), isOurCommit, + signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitSpend.SpendingTx, - chanState.ChanType, + chanState.ChanType, isRemoteInitiator, leaseExpiry, ) if err != nil { return nil, fmt.Errorf("unable to create htlc "+ @@ -5594,7 +5621,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si // locate the output index of our non-delayed output on the commitment // transaction. selfScript, maturityDelay, err := CommitScriptToRemote( - chanState.ChanType, keyRing.ToRemoteKey, + chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, + leaseExpiry, ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -5801,8 +5829,9 @@ type HtlcResolutions struct { func newOutgoingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, - feePerKw chainfee.SatPerKWeight, csvDelay uint32, - localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { + feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, + localCommit, isCommitFromInitiator bool, + chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -5854,8 +5883,9 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the fee calculated, re-construct the second level timeout // transaction. timeoutTx, err := CreateHtlcTimeoutTx( - chanType, op, secondLevelOutputAmt, htlc.RefundTimeout, - csvDelay, keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, isCommitFromInitiator, op, secondLevelOutputAmt, + htlc.RefundTimeout, csvDelay, leaseExpiry, keyRing.RevocationKey, + keyRing.ToLocalKey, ) if err != nil { return nil, err @@ -5901,7 +5931,8 @@ func newOutgoingHtlcResolution(signer input.Signer, // transaction creates so we can generate the signDesc required to // complete the claim process after a delay period. htlcSweepScript, err := SecondLevelHtlcScript( - keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + chanType, isCommitFromInitiator, keyRing.RevocationKey, + keyRing.ToLocalKey, csvDelay, leaseExpiry, ) if err != nil { return nil, err @@ -5942,8 +5973,9 @@ func newOutgoingHtlcResolution(signer input.Signer, func newIncomingHtlcResolution(signer input.Signer, localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, - feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool, - chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) { + feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, + localCommit, isCommitFromInitiator bool, chanType channeldb.ChannelType) ( + *IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -5988,8 +6020,8 @@ func newIncomingHtlcResolution(signer input.Signer, htlcFee := HtlcSuccessFee(chanType, feePerKw) secondLevelOutputAmt := htlc.Amt.ToSatoshis() - htlcFee successTx, err := CreateHtlcSuccessTx( - chanType, op, secondLevelOutputAmt, csvDelay, - keyRing.RevocationKey, keyRing.ToLocalKey, + chanType, isCommitFromInitiator, op, secondLevelOutputAmt, + csvDelay, leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, ) if err != nil { return nil, err @@ -6036,7 +6068,8 @@ func newIncomingHtlcResolution(signer input.Signer, // creates so we can generate the proper signDesc to sweep it after the // CSV delay has passed. htlcSweepScript, err := SecondLevelHtlcScript( - keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, + chanType, isCommitFromInitiator, keyRing.RevocationKey, + keyRing.ToLocalKey, csvDelay, leaseExpiry, ) if err != nil { return nil, err @@ -6096,8 +6129,8 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint { func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - commitTx *wire.MsgTx, chanType channeldb.ChannelType) ( - *HtlcResolutions, error) { + commitTx *wire.MsgTx, chanType channeldb.ChannelType, + isCommitFromInitiator bool, leaseExpiry uint32) (*HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -6129,8 +6162,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // as we can satisfy the contract. ihr, err := newIncomingHtlcResolution( signer, localChanCfg, commitTx, &htlc, - keyRing, feePerKw, uint32(csvDelay), ourCommit, - chanType, + keyRing, feePerKw, uint32(csvDelay), leaseExpiry, + ourCommit, isCommitFromInitiator, chanType, ) if err != nil { return nil, err @@ -6142,7 +6175,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitTx, &htlc, keyRing, - feePerKw, uint32(csvDelay), ourCommit, chanType, + feePerKw, uint32(csvDelay), leaseExpiry, ourCommit, + isCommitFromInitiator, chanType, ) if err != nil { return nil, err @@ -6281,8 +6315,13 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) + var leaseExpiry uint32 + if chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = chanState.ThawHeight + } toLocalScript, err := CommitScriptToSelf( - keyRing.ToLocalKey, keyRing.RevocationKey, csvTimeout, + chanState.ChanType, chanState.IsInitiator, keyRing.ToLocalKey, + keyRing.RevocationKey, csvTimeout, leaseExpiry, ) if err != nil { return nil, err @@ -6343,6 +6382,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitTx, chanState.ChanType, + chanState.IsInitiator, leaseExpiry, ) if err != nil { return nil, err diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 24bde047b..08cf96245 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -192,14 +192,31 @@ type ScriptInfo struct { // CommitScriptToSelf constructs the public key script for the output on the // commitment transaction paying to the "owner" of said commitment transaction. -// If the other party learns of the preimage to the revocation hash, then they -// can claim all the settled funds in the channel, plus the unsettled funds. -func CommitScriptToSelf(selfKey, revokeKey *btcec.PublicKey, csvDelay uint32) ( +// The `initiator` argument should correspond to the owner of the commitment +// tranasction which we are generating the to_local script for. If the other +// party learns of the preimage to the revocation hash, then they can claim all +// the settled funds in the channel, plus the unsettled funds. +func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, + selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32) ( *ScriptInfo, error) { - toLocalRedeemScript, err := input.CommitScriptToSelf( - csvDelay, selfKey, revokeKey, + var ( + toLocalRedeemScript []byte + err error ) + switch { + // If we are the initiator of a leased channel, then we have an + // additional CLTV requirement in addition to the usual CSV requirement. + case initiator && chanType.HasLeaseExpiration(): + toLocalRedeemScript, err = input.LeaseCommitScriptToSelf( + selfKey, revokeKey, csvDelay, leaseExpiry, + ) + + default: + toLocalRedeemScript, err = input.CommitScriptToSelf( + csvDelay, selfKey, revokeKey, + ) + } if err != nil { return nil, err } @@ -216,14 +233,38 @@ func CommitScriptToSelf(selfKey, revokeKey *btcec.PublicKey, csvDelay uint32) ( } // CommitScriptToRemote derives the appropriate to_remote script based on the -// channel's commitment type. The second return value is the CSV delay of the -// output script, what must be satisfied in order to spend the output. -func CommitScriptToRemote(chanType channeldb.ChannelType, - key *btcec.PublicKey) (*ScriptInfo, uint32, error) { +// channel's commitment type. The `initiator` argument should correspond to the +// owner of the commitment tranasction which we are generating the to_remote +// script for. The second return value is the CSV delay of the output script, +// what must be satisfied in order to spend the output. +func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, + key *btcec.PublicKey, leaseExpiry uint32) (*ScriptInfo, uint32, error) { + + switch { + // If we are not the initiator of a leased channel, then the remote + // party has an additional CLTV requirement in addition to the 1 block + // CSV requirement. + case chanType.HasLeaseExpiration() && !initiator: + script, err := input.LeaseCommitScriptToRemoteConfirmed( + key, leaseExpiry, + ) + if err != nil { + return nil, 0, err + } + + p2wsh, err := input.WitnessScriptHash(script) + if err != nil { + return nil, 0, err + } + + return &ScriptInfo{ + PkScript: p2wsh, + WitnessScript: script, + }, 1, nil // If this channel type has anchors, we derive the delayed to_remote // script. - if chanType.HasAnchors() { + case chanType.HasAnchors(): script, err := input.CommitScriptToRemoteConfirmed(key) if err != nil { return nil, 0, err @@ -238,20 +279,22 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, PkScript: p2wsh, WitnessScript: script, }, 1, nil - } - // Otherwise the to_remote will be a simple p2wkh. - p2wkh, err := input.CommitScriptUnencumbered(key) - if err != nil { - return nil, 0, err - } + default: + // Otherwise the to_remote will be a simple p2wkh. + p2wkh, err := input.CommitScriptUnencumbered(key) + if err != nil { + return nil, 0, err + } - // Since this is a regular P2WKH, the WitnessScipt and PkScript should - // both be set to the script hash. - return &ScriptInfo{ - WitnessScript: p2wkh, - PkScript: p2wkh, - }, 0, nil + // Since this is a regular P2WKH, the WitnessScipt and PkScript + // should both be set to the script hash. + return &ScriptInfo{ + WitnessScript: p2wkh, + PkScript: p2wkh, + }, 0, nil + + } } // HtlcSigHashType returns the sighash type to use for HTLC success and timeout @@ -296,13 +339,30 @@ func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 { // on the channel's commitment type. It is the uniform script that's used as the // output for the second-level HTLC transactions. The second level transaction // act as a sort of covenant, ensuring that a 2-of-2 multi-sig output can only -// be spent in a particular way, and to a particular output. -func SecondLevelHtlcScript(revocationKey, delayKey *btcec.PublicKey, - csvDelay uint32) (*ScriptInfo, error) { +// be spent in a particular way, and to a particular output. The `initiator` +// argument should correspond to the owner of the commitment tranasction which +// we are generating the to_local script for. +func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, + revocationKey, delayKey *btcec.PublicKey, + csvDelay, leaseExpiry uint32) (*ScriptInfo, error) { - witnessScript, err := input.SecondLevelHtlcScript( - revocationKey, delayKey, csvDelay, + var ( + witnessScript []byte + err error ) + switch { + // If we are the initiator of a leased channel, then we have an + // additional CLTV requirement in addition to the usual CSV requirement. + case initiator && chanType.HasLeaseExpiration(): + witnessScript, err = input.LeaseSecondLevelHtlcScript( + revocationKey, delayKey, csvDelay, leaseExpiry, + ) + + default: + witnessScript, err = input.SecondLevelHtlcScript( + revocationKey, delayKey, csvDelay, + ) + } if err != nil { return nil, err } @@ -549,19 +609,23 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // CreateCommitTx with parameters matching the perspective, to generate // a new commitment transaction with all the latest unsettled/un-timed // out HTLCs. + var leaseExpiry uint32 + if cb.chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = cb.chanState.ThawHeight + } if isOurs { commitTx, err = CreateCommitTx( cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), - numHTLCs, + numHTLCs, cb.chanState.IsInitiator, leaseExpiry, ) } else { commitTx, err = CreateCommitTx( cb.chanState.ChanType, fundingTxIn(cb.chanState), keyRing, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), - numHTLCs, + numHTLCs, !cb.chanState.IsInitiator, leaseExpiry, ) } if err != nil { @@ -660,12 +724,13 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, // output paying to the "owner" of the commitment transaction which can be // spent after a relative block delay or revocation event, and a remote output // paying the counterparty within the channel, which can be spent immediately -// or after a delay depending on the commitment type.. +// or after a delay depending on the commitment type. The `initiator` argument +// should correspond to the owner of the commitment tranasction we are creating. func CreateCommitTx(chanType channeldb.ChannelType, fundingOutput wire.TxIn, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, amountToLocal, amountToRemote btcutil.Amount, - numHTLCs int64) (*wire.MsgTx, error) { + numHTLCs int64, initiator bool, leaseExpiry uint32) (*wire.MsgTx, error) { // First, we create the script for the delayed "pay-to-self" output. // This output has 2 main redemption clauses: either we can redeem the @@ -673,8 +738,8 @@ func CreateCommitTx(chanType channeldb.ChannelType, // the funds with the revocation key if we broadcast a revoked // commitment transaction. toLocalScript, err := CommitScriptToSelf( - keyRing.ToLocalKey, keyRing.RevocationKey, - uint32(localChanCfg.CsvDelay), + chanType, initiator, keyRing.ToLocalKey, keyRing.RevocationKey, + uint32(localChanCfg.CsvDelay), leaseExpiry, ) if err != nil { return nil, err @@ -682,7 +747,7 @@ func CreateCommitTx(chanType channeldb.ChannelType, // Next, we create the script paying to the remote. toRemoteScript, _, err := CommitScriptToRemote( - chanType, keyRing.ToRemoteKey, + chanType, initiator, keyRing.ToRemoteKey, leaseExpiry, ) if err != nil { return nil, err diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go index ab6e5e1a4..c33430c21 100644 --- a/lnwallet/test_utils.go +++ b/lnwallet/test_utils.go @@ -119,6 +119,7 @@ func CreateTestChannels(chanType channeldb.ChannelType) ( channelBal := channelCapacity / 2 csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) + isAliceInitiator := true prevOut := &wire.OutPoint{ Hash: chainhash.Hash(testHdSeed), @@ -223,7 +224,7 @@ func CreateTestChannels(chanType channeldb.ChannelType) ( aliceCommitTx, bobCommitTx, err := CreateCommitmentTxns( channelBal, channelBal, &aliceCfg, &bobCfg, aliceCommitPoint, - bobCommitPoint, *fundingTxIn, chanType, + bobCommitPoint, *fundingTxIn, chanType, isAliceInitiator, 0, ) if err != nil { return nil, nil, nil, err @@ -318,7 +319,7 @@ func CreateTestChannels(chanType channeldb.ChannelType) ( FundingOutpoint: *prevOut, ShortChannelID: shortChanID, ChanType: chanType, - IsInitiator: true, + IsInitiator: isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: bobCommitPoint, RevocationProducer: alicePreimageProducer, @@ -336,7 +337,7 @@ func CreateTestChannels(chanType channeldb.ChannelType) ( FundingOutpoint: *prevOut, ShortChannelID: shortChanID, ChanType: chanType, - IsInitiator: false, + IsInitiator: !isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: aliceCommitPoint, RevocationProducer: bobPreimageProducer, diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index 2072deb6a..ce2eb9e80 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -44,9 +44,10 @@ var ( // In order to spend the HTLC output, the witness for the passed transaction // should be: // * <0> -func CreateHtlcSuccessTx(chanType channeldb.ChannelType, - htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay uint32, - revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { +func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, + htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay, + leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) ( + *wire.MsgTx, error) { // Create a version two transaction (as the success version of this // spends an output with a CSV timeout). @@ -65,7 +66,8 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, // level HTLC which forces a covenant w.r.t what can be done with all // HTLC outputs. script, err := SecondLevelHtlcScript( - revocationKey, delayKey, csvDelay, + chanType, initiator, revocationKey, delayKey, csvDelay, + leaseExpiry, ) if err != nil { return nil, err @@ -97,9 +99,9 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, // NOTE: The passed amount for the HTLC should take into account the required // fee rate at the time the HTLC was created. The fee should be able to // entirely pay for this (tiny: 1-in 1-out) transaction. -func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, +func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, - cltvExpiry, csvDelay uint32, + cltvExpiry, csvDelay, leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this @@ -123,7 +125,8 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, // level HTLC which forces a covenant w.r.t what can be done with all // HTLC outputs. script, err := SecondLevelHtlcScript( - revocationKey, delayKey, csvDelay, + chanType, initiator, revocationKey, delayKey, csvDelay, + leaseExpiry, ) if err != nil { return nil, err diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go index a0b6c4311..de73d6f33 100644 --- a/lnwallet/transactions_test.go +++ b/lnwallet/transactions_test.go @@ -573,7 +573,7 @@ func testSpendValidation(t *testing.T, tweakless bool) { } commitmentTx, err := CreateCommitTx( channelType, *fakeFundingTxIn, keyRing, aliceChanCfg, - bobChanCfg, channelBalance, channelBalance, 0, + bobChanCfg, channelBalance, channelBalance, 0, true, 0, ) if err != nil { t.Fatalf("unable to create commitment transaction: %v", nil) @@ -889,7 +889,7 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp remoteCommitTx, localCommitTx, err := CreateCommitmentTxns( remoteBalance, localBalance-commitFee, &remoteCfg, &localCfg, remoteCommitPoint, - localCommitPoint, *fundingTxIn, chanType, + localCommitPoint, *fundingTxIn, chanType, true, 0, ) require.NoError(t, err) diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go index 32d094053..feb8b0cd9 100644 --- a/lnwallet/wallet.go +++ b/lnwallet/wallet.go @@ -1246,8 +1246,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourChanCfg, theirChanCfg *channeldb.ChannelConfig, localCommitPoint, remoteCommitPoint *btcec.PublicKey, - fundingTxIn wire.TxIn, chanType channeldb.ChannelType) ( - *wire.MsgTx, *wire.MsgTx, error) { + fundingTxIn wire.TxIn, chanType channeldb.ChannelType, initiator bool, + leaseExpiry uint32) (*wire.MsgTx, *wire.MsgTx, error) { localCommitmentKeys := DeriveCommitmentKeys( localCommitPoint, true, chanType, ourChanCfg, theirChanCfg, @@ -1258,7 +1258,8 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, ourCommitTx, err := CreateCommitTx( chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, - theirChanCfg, localBalance, remoteBalance, 0, + theirChanCfg, localBalance, remoteBalance, 0, initiator, + leaseExpiry, ) if err != nil { return nil, nil, err @@ -1271,7 +1272,8 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount, theirCommitTx, err := CreateCommitTx( chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, - ourChanCfg, remoteBalance, localBalance, 0, + ourChanCfg, remoteBalance, localBalance, 0, !initiator, + leaseExpiry, ) if err != nil { return nil, nil, err @@ -1522,12 +1524,17 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) { // With the funding tx complete, create both commitment transactions. localBalance := pendingReservation.partialState.LocalCommitment.LocalBalance.ToSatoshis() remoteBalance := pendingReservation.partialState.LocalCommitment.RemoteBalance.ToSatoshis() + var leaseExpiry uint32 + if pendingReservation.partialState.ChanType.HasLeaseExpiration() { + leaseExpiry = pendingReservation.partialState.ThawHeight + } ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, ourContribution.ChannelConfig, theirContribution.ChannelConfig, ourContribution.FirstCommitmentPoint, theirContribution.FirstCommitmentPoint, fundingTxIn, pendingReservation.partialState.ChanType, + pendingReservation.partialState.IsInitiator, leaseExpiry, ) if err != nil { req.err <- err @@ -1894,6 +1901,10 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { // remote node's commitment transactions. localBalance := pendingReservation.partialState.LocalCommitment.LocalBalance.ToSatoshis() remoteBalance := pendingReservation.partialState.LocalCommitment.RemoteBalance.ToSatoshis() + var leaseExpiry uint32 + if pendingReservation.partialState.ChanType.HasLeaseExpiration() { + leaseExpiry = pendingReservation.partialState.ThawHeight + } ourCommitTx, theirCommitTx, err := CreateCommitmentTxns( localBalance, remoteBalance, pendingReservation.ourContribution.ChannelConfig, @@ -1901,6 +1912,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) { pendingReservation.ourContribution.FirstCommitmentPoint, pendingReservation.theirContribution.FirstCommitmentPoint, *fundingTxIn, pendingReservation.partialState.ChanType, + pendingReservation.partialState.IsInitiator, leaseExpiry, ) if err != nil { req.err <- err diff --git a/peer/test_utils.go b/peer/test_utils.go index f69e42e57..a29cb7e53 100644 --- a/peer/test_utils.go +++ b/peer/test_utils.go @@ -79,6 +79,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, bobDustLimit := btcutil.Amount(1300) csvTimeoutAlice := uint32(5) csvTimeoutBob := uint32(4) + isAliceInitiator := true prevOut := &wire.OutPoint{ Hash: channels.TestHdSeed, @@ -162,6 +163,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, aliceCommitTx, bobCommitTx, err := lnwallet.CreateCommitmentTxns( channelBal, channelBal, &aliceCfg, &bobCfg, aliceCommitPoint, bobCommitPoint, *fundingTxIn, channeldb.SingleFunderTweaklessBit, + isAliceInitiator, 0, ) if err != nil { return nil, nil, nil, err @@ -229,7 +231,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, FundingOutpoint: *prevOut, ShortChannelID: shortChanID, ChanType: channeldb.SingleFunderTweaklessBit, - IsInitiator: true, + IsInitiator: isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: bobCommitPoint, RevocationProducer: alicePreimageProducer, @@ -246,7 +248,7 @@ func createTestPeer(notifier chainntnfs.ChainNotifier, IdentityPub: bobKeyPub, FundingOutpoint: *prevOut, ChanType: channeldb.SingleFunderTweaklessBit, - IsInitiator: false, + IsInitiator: !isAliceInitiator, Capacity: channelCapacity, RemoteCurrentRevocation: aliceCommitPoint, RevocationProducer: bobPreimageProducer,