diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 4012f031d..b1e3fc1c2 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -426,15 +426,36 @@ func (c *chainWatcher) handleUnknownLocalState( &c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg, ) + auxResult, err := fn.MapOptionZ( + c.cfg.auxLeafStore, + //nolint:lll + func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + lnwallet.NewAuxChanState(c.cfg.chanState), + c.cfg.chanState.LocalCommitment, *commitKeyRing, + ) + }, + ).Unpack() + if err != nil { + return false, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // 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 } + + remoteAuxLeaf := fn.ChainOption( + func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + }, + )(auxResult.AuxLeaves) remoteScript, _, err := lnwallet.CommitScriptToRemote( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, - commitKeyRing.ToRemoteKey, leaseExpiry, input.NoneTapLeaf(), + commitKeyRing.ToRemoteKey, leaseExpiry, + remoteAuxLeaf, ) if err != nil { return false, err @@ -443,11 +464,16 @@ func (c *chainWatcher) handleUnknownLocalState( // Next, we'll derive our script that includes the revocation base for // the remote party allowing them to claim this output before the CSV // delay if we breach. + localAuxLeaf := fn.ChainOption( + func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + }, + )(auxResult.AuxLeaves) localScript, err := lnwallet.CommitScriptToSelf( c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey, uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry, - input.NoneTapLeaf(), + localAuxLeaf, ) if err != nil { return false, err diff --git a/input/size_test.go b/input/size_test.go index 21e2c06ae..33f9ff539 100644 --- a/input/size_test.go +++ b/input/size_test.go @@ -1388,7 +1388,7 @@ func genTimeoutTx(t *testing.T, // Create the unsigned timeout tx. timeoutTx, err := lnwallet.CreateHtlcTimeoutTx( chanType, false, testOutPoint, testAmt, testCLTVExpiry, - testCSVDelay, 0, testPubkey, testPubkey, + testCSVDelay, 0, testPubkey, testPubkey, input.NoneTapLeaf(), ) require.NoError(t, err) @@ -1457,7 +1457,7 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx { // Create the unsigned success tx. successTx, err := lnwallet.CreateHtlcSuccessTx( chanType, false, testOutPoint, testAmt, testCSVDelay, 0, - testPubkey, testPubkey, + testPubkey, testPubkey, input.NoneTapLeaf(), ) require.NoError(t, err) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index c382b9dc2..e5cbe6b6a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -576,7 +576,8 @@ func (c *commitment) toDiskCommit( // restart a channel session. func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, htlc *channeldb.HTLC, commitKeys lntypes.Dual[*CommitmentKeyRing], - whoseCommit lntypes.ChannelParty) (PaymentDescriptor, error) { + whoseCommit lntypes.ChannelParty, + auxLeaf input.AuxTapLeaf) (PaymentDescriptor, error) { // The proper pkScripts for this PaymentDescriptor must be // generated so we can easily locate them within the commitment @@ -602,6 +603,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, scriptInfo, err := genHtlcScript( chanType, htlc.Incoming, lntypes.Local, htlc.RefundTimeout, htlc.RHash, localCommitKeys, + auxLeaf, ) if err != nil { return pd, err @@ -618,6 +620,7 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, scriptInfo, err := genHtlcScript( chanType, htlc.Incoming, lntypes.Remote, htlc.RefundTimeout, htlc.RHash, remoteCommitKeys, + auxLeaf, ) if err != nil { return pd, err @@ -667,7 +670,8 @@ func (lc *LightningChannel) diskHtlcToPayDesc(feeRate chainfee.SatPerKWeight, // for each side. func (lc *LightningChannel) extractPayDescs(feeRate chainfee.SatPerKWeight, htlcs []channeldb.HTLC, commitKeys lntypes.Dual[*CommitmentKeyRing], - whoseCommit lntypes.ChannelParty) ([]PaymentDescriptor, + whoseCommit lntypes.ChannelParty, + auxLeaves fn.Option[CommitAuxLeaves]) ([]PaymentDescriptor, []PaymentDescriptor, error) { var ( @@ -685,8 +689,19 @@ func (lc *LightningChannel) extractPayDescs(feeRate chainfee.SatPerKWeight, htlc := htlc + auxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.OutgoingHtlcLeaves + if htlc.Incoming { + leaves = l.IncomingHtlcLeaves + } + + return leaves[htlc.HtlcIndex].AuxTapLeaf + }, + )(auxLeaves) + payDesc, err := lc.diskHtlcToPayDesc( - feeRate, &htlc, commitKeys, whoseCommit, + feeRate, &htlc, commitKeys, whoseCommit, auxLeaf, ) if err != nil { return incomingHtlcs, outgoingHtlcs, err @@ -733,12 +748,25 @@ func (lc *LightningChannel) diskCommitToMemCommit( )) } + auxResult, err := fn.MapOptionZ( + lc.leafStore, + func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(lc.channelState), *diskCommit, + *commitKeys.GetForParty(whoseCommit), + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // With the key rings re-created, we'll now convert all the on-disk // HTLC"s into PaymentDescriptor's so we can re-insert them into our // update log. incomingHtlcs, outgoingHtlcs, err := lc.extractPayDescs( chainfee.SatPerKWeight(diskCommit.FeePerKw), - diskCommit.Htlcs, commitKeys, whoseCommit, + diskCommit.Htlcs, commitKeys, whoseCommit, auxResult.AuxLeaves, ) if err != nil { return nil, err @@ -1091,7 +1119,8 @@ func (lc *LightningChannel) ResetState() { func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, remoteUpdateLog *updateLog, commitHeight uint64, feeRate chainfee.SatPerKWeight, remoteCommitKeys *CommitmentKeyRing, - remoteDustLimit btcutil.Amount) (*PaymentDescriptor, error) { + remoteDustLimit btcutil.Amount, + auxLeaves fn.Option[CommitAuxLeaves]) (*PaymentDescriptor, error) { // Depending on the type of update message we'll map that to a distinct // PaymentDescriptor instance. @@ -1127,10 +1156,17 @@ func (lc *LightningChannel) logUpdateToPayDesc(logUpdate *channeldb.LogUpdate, feeRate, wireMsg.Amount.ToSatoshis(), remoteDustLimit, ) if !isDustRemote { + auxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.OutgoingHtlcLeaves + return leaves[pd.HtlcIndex].AuxTapLeaf + }, + )(auxLeaves) + scriptInfo, err := genHtlcScript( lc.channelState.ChanType, false, lntypes.Remote, wireMsg.Expiry, wireMsg.PaymentHash, - remoteCommitKeys, + remoteCommitKeys, auxLeaf, ) if err != nil { return nil, err @@ -1791,6 +1827,19 @@ func (lc *LightningChannel) restorePendingLocalUpdates( pendingCommit := pendingRemoteCommitDiff.Commitment pendingHeight := pendingCommit.CommitHeight + auxResult, err := fn.MapOptionZ( + lc.leafStore, + func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(lc.channelState), pendingCommit, + *pendingRemoteKeys, + ) + }, + ).Unpack() + if err != nil { + return fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // If we did have a dangling commit, then we'll examine which updates // we included in that state and re-insert them into our update log. for _, logUpdate := range pendingRemoteCommitDiff.LogUpdates { @@ -1801,6 +1850,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates( chainfee.SatPerKWeight(pendingCommit.FeePerKw), pendingRemoteKeys, lc.channelState.RemoteChanCfg.DustLimit, + auxResult.AuxLeaves, ) if err != nil { return err @@ -2007,24 +2057,40 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, leaseExpiry = chanState.ThawHeight } - // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). + auxResult, err := fn.MapOptionZ( + leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromRevocation(revokedLog) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } // Since it is the remote breach we are reconstructing, the output // going to us will be a to-remote script with our local params. + remoteAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + }, + )(auxResult.AuxLeaves) isRemoteInitiator := !chanState.IsInitiator ourScript, ourDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, input.NoneTapLeaf(), + leaseExpiry, remoteAuxLeaf, ) if err != nil { return nil, err } + localAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + }, + )(auxResult.AuxLeaves) theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) theirScript, err := CommitScriptToSelf( chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, - keyRing.RevocationKey, theirDelay, leaseExpiry, - input.NoneTapLeaf(), + keyRing.RevocationKey, theirDelay, leaseExpiry, localAuxLeaf, ) if err != nil { return nil, err @@ -2042,7 +2108,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, if revokedLog != nil { br, ourAmt, theirAmt, err = createBreachRetribution( revokedLog, spendTx, chanState, keyRing, - commitmentSecret, leaseExpiry, + commitmentSecret, leaseExpiry, auxResult.AuxLeaves, ) if err != nil { return nil, err @@ -2176,7 +2242,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, func createHtlcRetribution(chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing, commitHash chainhash.Hash, commitmentSecret *btcec.PrivateKey, leaseExpiry uint32, - htlc *channeldb.HTLCEntry) (HtlcRetribution, error) { + htlc *channeldb.HTLCEntry, + auxLeaves fn.Option[CommitAuxLeaves]) (HtlcRetribution, error) { var emptyRetribution HtlcRetribution @@ -2186,10 +2253,24 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, // We'll generate the original second level witness script now, as // we'll need it if we're revoking an HTLC output on the remote // commitment transaction, and *they* go to the second level. + secondLevelAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) fn.Option[input.AuxTapLeaf] { + return fn.MapOption(func(val uint16) input.AuxTapLeaf { + idx := input.HtlcIndex(val) + + if htlc.Incoming.Val { + leaves := l.IncomingHtlcLeaves[idx] + return leaves.SecondLevelLeaf + } + + return l.OutgoingHtlcLeaves[idx].SecondLevelLeaf + })(htlc.HtlcIndex.ValOpt()) + }, + )(auxLeaves) secondLevelScript, err := SecondLevelHtlcScript( chanState.ChanType, isRemoteInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay, - leaseExpiry, + leaseExpiry, fn.FlattenOption(secondLevelAuxLeaf), ) if err != nil { return emptyRetribution, err @@ -2200,9 +2281,24 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, // HTLC script. Otherwise, is this was an outgoing HTLC that we sent, // then from the PoV of the remote commitment state, they're the // receiver of this HTLC. + htlcLeaf := fn.ChainOption( + func(l CommitAuxLeaves) fn.Option[input.AuxTapLeaf] { + return fn.MapOption(func(val uint16) input.AuxTapLeaf { + idx := input.HtlcIndex(val) + + if htlc.Incoming.Val { + leaves := l.IncomingHtlcLeaves[idx] + return leaves.AuxTapLeaf + } + + return l.OutgoingHtlcLeaves[idx].AuxTapLeaf + })(htlc.HtlcIndex.ValOpt()) + }, + )(auxLeaves) scriptInfo, err := genHtlcScript( chanState.ChanType, htlc.Incoming.Val, lntypes.Remote, htlc.RefundTimeout.Val, htlc.RHash.Val, keyRing, + fn.FlattenOption(htlcLeaf), ) if err != nil { return emptyRetribution, err @@ -2266,7 +2362,9 @@ func createHtlcRetribution(chanState *channeldb.OpenChannel, func createBreachRetribution(revokedLog *channeldb.RevocationLog, spendTx *wire.MsgTx, chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing, commitmentSecret *btcec.PrivateKey, - leaseExpiry uint32) (*BreachRetribution, int64, int64, error) { + leaseExpiry uint32, + auxLeaves fn.Option[CommitAuxLeaves]) (*BreachRetribution, int64, int64, + error) { commitHash := revokedLog.CommitTxHash @@ -2275,7 +2373,7 @@ func createBreachRetribution(revokedLog *channeldb.RevocationLog, for i, htlc := range revokedLog.HTLCEntries { hr, err := createHtlcRetribution( chanState, keyRing, commitHash.Val, - commitmentSecret, leaseExpiry, htlc, + commitmentSecret, leaseExpiry, htlc, auxLeaves, ) if err != nil { return nil, 0, 0, err @@ -2427,6 +2525,7 @@ func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment, hr, err := createHtlcRetribution( chanState, keyRing, commitHash, commitmentSecret, leaseExpiry, entry, + fn.None[CommitAuxLeaves](), ) if err != nil { return nil, 0, 0, err @@ -3041,13 +3140,27 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // With the keys generated, we'll make a slice with enough capacity to // hold potentially all the HTLCs. The actual slice may be a bit // smaller (than its total capacity) and some HTLCs may be dust. - numSigs := (len(remoteCommitView.incomingHTLCs) + - len(remoteCommitView.outgoingHTLCs)) + numSigs := len(remoteCommitView.incomingHTLCs) + + len(remoteCommitView.outgoingHTLCs) sigBatch := make([]SignJob, 0, numSigs) var err error cancelChan := make(chan struct{}) + auxResult, err := fn.MapOptionZ( + leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(chanState), + *remoteCommitView.toDiskCommit(lntypes.Remote), + *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w", + err) + } + // For each outgoing and incoming HTLC, if the HTLC isn't considered a // dust output after taking into account second-level HTLC fees, then a // sigJob will be generated and appended to the current batch. @@ -3074,6 +3187,13 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.IncomingHtlcLeaves + return leaves[htlc.HtlcIndex].SecondLevelLeaf + }, + )(auxResult.AuxLeaves) + // With the fee calculate, we can properly create the HTLC // timeout transaction using the HTLC amount minus the fee. op := wire.OutPoint{ @@ -3084,11 +3204,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanType, isRemoteInitiator, op, outputAmt, htlc.Timeout, uint32(remoteChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + auxLeaf, ) if err != nil { return nil, nil, err } + // TODO(roasbeef): hook up signer interface here (later commit + // in this PR). + // Construct a full hash cache as we may be signing a segwit v1 // sighash. txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex] @@ -3115,7 +3239,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, // If this is a taproot channel, then we'll need to set the // method type to ensure we generate a valid signature. if chanType.IsTaproot() { - sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll + //nolint:lll + sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod } sigBatch = append(sigBatch, sigJob) @@ -3141,6 +3266,13 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.OutgoingHtlcLeaves + return leaves[htlc.HtlcIndex].SecondLevelLeaf + }, + )(auxResult.AuxLeaves) + // With the proper output amount calculated, we can now // generate the success transaction using the remote party's // CSV delay. @@ -3152,6 +3284,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing, chanType, isRemoteInitiator, op, outputAmt, uint32(remoteChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, keyRing.ToLocalKey, + auxLeaf, ) if err != nil { return nil, nil, err @@ -4491,10 +4624,24 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, // enough capacity to hold verification jobs for all HTLC's in this // view. In the case that we have some dust outputs, then the actual // length will be smaller than the total capacity. - numHtlcs := (len(localCommitmentView.incomingHTLCs) + - len(localCommitmentView.outgoingHTLCs)) + numHtlcs := len(localCommitmentView.incomingHTLCs) + + len(localCommitmentView.outgoingHTLCs) verifyJobs := make([]VerifyJob, 0, numHtlcs) + auxResult, err := fn.MapOptionZ( + leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(chanState), + *localCommitmentView.toDiskCommit( + lntypes.Local, + ), *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // We'll iterate through each output in the commitment transaction, // populating the sigHash closure function if it's detected to be an // HLTC output. Given the sighash, and the signing key, we'll be able @@ -4528,11 +4675,19 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcSuccessFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.ChainOption(func( + l CommitAuxLeaves) input.AuxTapLeaf { + + leaves := l.IncomingHtlcLeaves + idx := htlc.HtlcIndex + return leaves[idx].SecondLevelLeaf + })(auxResult.AuxLeaves) + successTx, err := CreateHtlcSuccessTx( chanType, isLocalInitiator, op, outputAmt, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -4612,12 +4767,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel, htlcFee := HtlcTimeoutFee(chanType, feePerKw) outputAmt := htlc.Amount.ToSatoshis() - htlcFee + auxLeaf := fn.ChainOption(func( + l CommitAuxLeaves) input.AuxTapLeaf { + + leaves := l.OutgoingHtlcLeaves + idx := htlc.HtlcIndex + return leaves[idx].SecondLevelLeaf + })(auxResult.AuxLeaves) + timeoutTx, err := CreateHtlcTimeoutTx( chanType, isLocalInitiator, op, outputAmt, htlc.Timeout, uint32(localChanCfg.CsvDelay), leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + keyRing.ToLocalKey, auxLeaf, ) if err != nil { return nil, err @@ -6332,6 +6495,18 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) + auxResult, err := fn.MapOptionZ( + leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(chanState), remoteCommit, + *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } + // Next, we'll obtain HTLC resolutions for all the outgoing HTLC's we // had on their commitment transaction. var leaseExpiry uint32 @@ -6344,6 +6519,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitSpend.SpendingTx, chanState.ChanType, isRemoteInitiator, leaseExpiry, + auxResult.AuxLeaves, ) if err != nil { return nil, fmt.Errorf("unable to create htlc "+ @@ -6352,14 +6528,17 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, commitTxBroadcast := commitSpend.SpendingTx - // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). - // Before we can generate the proper sign descriptor, we'll need to // locate the output index of our non-delayed output on the commitment // transaction. + remoteAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.RemoteAuxLeaf + }, + )(auxResult.AuxLeaves) selfScript, maturityDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, - leaseExpiry, input.NoneTapLeaf(), + leaseExpiry, remoteAuxLeaf, ) if err != nil { return nil, fmt.Errorf("unable to create self commit "+ @@ -6599,7 +6778,8 @@ func newOutgoingHtlcResolution(signer input.Signer, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool, - chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { + chanType channeldb.ChannelType, + auxLeaves fn.Option[CommitAuxLeaves]) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -6608,9 +6788,12 @@ func newOutgoingHtlcResolution(signer input.Signer, // First, we'll re-generate the script used to send the HTLC to the // remote party within their commitment transaction. + auxLeaf := fn.ChainOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.OutgoingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + })(auxLeaves) htlcScriptInfo, err := genHtlcScript( chanType, false, whoseCommit, htlc.RefundTimeout, htlc.RHash, - keyRing, + keyRing, auxLeaf, ) if err != nil { return nil, err @@ -6683,10 +6866,16 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the fee calculated, re-construct the second level timeout // transaction. + secondLevelAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.OutgoingHtlcLeaves + return leaves[htlc.HtlcIndex].SecondLevelLeaf + }, + )(auxLeaves) timeoutTx, err := CreateHtlcTimeoutTx( chanType, isCommitFromInitiator, op, secondLevelOutputAmt, - htlc.RefundTimeout, csvDelay, leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + htlc.RefundTimeout, csvDelay, leaseExpiry, + keyRing.RevocationKey, keyRing.ToLocalKey, secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -6769,6 +6958,7 @@ func newOutgoingHtlcResolution(signer input.Signer, htlcSweepScript, err = SecondLevelHtlcScript( chanType, isCommitFromInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, leaseExpiry, + secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -6777,7 +6967,7 @@ func newOutgoingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, - fn.None[txscript.TapLeaf](), + secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -6852,8 +7042,8 @@ func newIncomingHtlcResolution(signer input.Signer, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32, whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool, - chanType channeldb.ChannelType) ( - *IncomingHtlcResolution, error) { + chanType channeldb.ChannelType, + auxLeaves fn.Option[CommitAuxLeaves]) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ Hash: commitTx.TxHash(), @@ -6862,9 +7052,12 @@ func newIncomingHtlcResolution(signer input.Signer, // First, we'll re-generate the script the remote party used to // send the HTLC to us in their commitment transaction. + auxLeaf := fn.ChainOption(func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.IncomingHtlcLeaves[htlc.HtlcIndex].AuxTapLeaf + })(auxLeaves) scriptInfo, err := genHtlcScript( chanType, true, whoseCommit, htlc.RefundTimeout, htlc.RHash, - keyRing, + keyRing, auxLeaf, ) if err != nil { return nil, err @@ -6924,6 +7117,13 @@ func newIncomingHtlcResolution(signer input.Signer, }, nil } + secondLevelAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + leaves := l.IncomingHtlcLeaves + return leaves[htlc.HtlcIndex].SecondLevelLeaf + }, + )(auxLeaves) + // Otherwise, we'll need to go to the second level to sweep this HTLC. // // First, we'll reconstruct the original HTLC success transaction, @@ -6933,7 +7133,7 @@ func newIncomingHtlcResolution(signer input.Signer, successTx, err := CreateHtlcSuccessTx( chanType, isCommitFromInitiator, op, secondLevelOutputAmt, csvDelay, leaseExpiry, keyRing.RevocationKey, - keyRing.ToLocalKey, + keyRing.ToLocalKey, secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -7016,6 +7216,7 @@ func newIncomingHtlcResolution(signer input.Signer, htlcSweepScript, err = SecondLevelHtlcScript( chanType, isCommitFromInitiator, keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, leaseExpiry, + secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -7024,7 +7225,7 @@ func newIncomingHtlcResolution(signer input.Signer, //nolint:lll secondLevelScriptTree, err := input.TaprootSecondLevelScriptTree( keyRing.RevocationKey, keyRing.ToLocalKey, csvDelay, - fn.None[txscript.TapLeaf](), + secondLevelAuxLeaf, ) if err != nil { return nil, err @@ -7117,7 +7318,8 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, chanType channeldb.ChannelType, - isCommitFromInitiator bool, leaseExpiry uint32) (*HtlcResolutions, error) { + isCommitFromInitiator bool, leaseExpiry uint32, + auxLeaves fn.Option[CommitAuxLeaves]) (*HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? dustLimit := remoteChanCfg.DustLimit @@ -7150,8 +7352,9 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, // as we can satisfy the contract. ihr, err := newIncomingHtlcResolution( signer, localChanCfg, commitTx, &htlc, - keyRing, feePerKw, uint32(csvDelay), leaseExpiry, - whoseCommit, isCommitFromInitiator, chanType, + keyRing, feePerKw, uint32(csvDelay), + leaseExpiry, whoseCommit, isCommitFromInitiator, + chanType, auxLeaves, ) if err != nil { return nil, fmt.Errorf("incoming resolution "+ @@ -7165,7 +7368,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ohr, err := newOutgoingHtlcResolution( signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), leaseExpiry, whoseCommit, - isCommitFromInitiator, chanType, + isCommitFromInitiator, chanType, auxLeaves, ) if err != nil { return nil, fmt.Errorf("outgoing resolution "+ @@ -7304,16 +7507,31 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, ) - // TODO(roasbeef): Actually fetch aux leaves (later commits in this PR). + auxResult, err := fn.MapOptionZ( + leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] { + return s.FetchLeavesFromCommit( + NewAuxChanState(chanState), + chanState.LocalCommitment, *keyRing, + ) + }, + ).Unpack() + if err != nil { + return nil, fmt.Errorf("unable to fetch aux leaves: %w", err) + } var leaseExpiry uint32 if chanState.ChanType.HasLeaseExpiration() { leaseExpiry = chanState.ThawHeight } + + localAuxLeaf := fn.ChainOption( + func(l CommitAuxLeaves) input.AuxTapLeaf { + return l.LocalAuxLeaf + }, + )(auxResult.AuxLeaves) toLocalScript, err := CommitScriptToSelf( chanState.ChanType, chanState.IsInitiator, keyRing.ToLocalKey, - keyRing.RevocationKey, csvTimeout, leaseExpiry, - input.NoneTapLeaf(), + keyRing.RevocationKey, csvTimeout, leaseExpiry, localAuxLeaf, ) if err != nil { return nil, err @@ -7404,7 +7622,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, chainfee.SatPerKWeight(localCommit.FeePerKw), lntypes.Local, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, &chanState.RemoteChanCfg, commitTx, chanState.ChanType, - chanState.IsInitiator, leaseExpiry, + chanState.IsInitiator, leaseExpiry, auxResult.AuxLeaves, ) if err != nil { return nil, fmt.Errorf("unable to gen htlc resolution: %w", err) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 4a1cb0321..de6bb219c 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -9971,7 +9971,7 @@ func TestCreateHtlcRetribution(t *testing.T) { // Create the htlc retribution. hr, err := createHtlcRetribution( aliceChannel.channelState, keyRing, commitHash, - dummyPrivate, leaseExpiry, htlc, + dummyPrivate, leaseExpiry, htlc, fn.None[CommitAuxLeaves](), ) // Expect no error. require.NoError(t, err) @@ -10177,6 +10177,7 @@ func TestCreateBreachRetribution(t *testing.T) { tc.revocationLog, tx, aliceChannel.channelState, keyRing, dummyPrivate, leaseExpiry, + fn.None[CommitAuxLeaves](), ) // Check the error if expected. @@ -10295,7 +10296,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { // error as there are no past delta state saved as revocation logs yet. _, err = NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, breachTx, - fn.None[AuxLeafStore](), + fn.Some[AuxLeafStore](&MockAuxLeafStore{}), ) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) @@ -10303,7 +10304,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { // provided. _, err = NewBreachRetribution( aliceChannel.channelState, stateNum, breachHeight, nil, - fn.None[AuxLeafStore](), + fn.Some[AuxLeafStore](&MockAuxLeafStore{}), ) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) @@ -10417,6 +10418,7 @@ func TestExtractPayDescs(t *testing.T) { // scripts(`genHtlcScript`) as it should be tested independently. incomingPDs, outgoingPDs, err := lnChan.extractPayDescs( 0, htlcs, lntypes.Dual[*CommitmentKeyRing]{}, lntypes.Local, + fn.None[CommitAuxLeaves](), ) require.NoError(t, err) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 7f2a9ce33..2ff23ab63 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -420,14 +420,14 @@ func sweepSigHash(chanType channeldb.ChannelType) txscript.SigHashType { // argument should correspond to the owner of the commitment transaction which // we are generating the to_local script for. func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, - revocationKey, delayKey *btcec.PublicKey, - csvDelay, leaseExpiry uint32) (input.ScriptDescriptor, error) { + revocationKey, delayKey *btcec.PublicKey, csvDelay, leaseExpiry uint32, + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { switch { // For taproot channels, the pkScript is a segwit v1 p2tr output. case chanType.IsTaproot(): return input.TaprootSecondLevelScriptTree( - revocationKey, delayKey, csvDelay, input.NoneTapLeaf(), + revocationKey, delayKey, csvDelay, auxLeaf, ) // If we are the initiator of a leased channel, then we have an @@ -755,10 +755,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, theirBalance -= commitFeeMSat } - var ( - commitTx *wire.MsgTx - err error - ) + var commitTx *wire.MsgTx // Before we create the commitment transaction below, we'll try to see // if there're any aux leaves that need to be a part of the tapscript @@ -806,6 +803,19 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, return nil, err } + // Similarly, we'll now attempt to extract the set of aux leaves for + // the set of incoming and outgoing HTLCs. + incomingAuxLeaves := fn.MapOption( + func(leaves CommitAuxLeaves) input.HtlcAuxLeaves { + return leaves.IncomingHtlcLeaves + }, + )(auxResult.AuxLeaves) + outgoingAuxLeaves := fn.MapOption( + func(leaves CommitAuxLeaves) input.HtlcAuxLeaves { + return leaves.OutgoingHtlcLeaves + }, + )(auxResult.AuxLeaves) + // We'll now add all the HTLC outputs to the commitment transaction. // Each output includes an off-chain 2-of-2 covenant clause, so we'll // need the objective local/remote keys for this particular commitment @@ -826,9 +836,15 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } + auxLeaf := fn.ChainOption( + func(leaves input.HtlcAuxLeaves) input.AuxTapLeaf { + return leaves[htlc.HtlcIndex].AuxTapLeaf + }, + )(outgoingAuxLeaves) + err := addHTLC( commitTx, whoseCommit, false, htlc, keyRing, - cb.chanState.ChanType, + cb.chanState.ChanType, auxLeaf, ) if err != nil { return nil, err @@ -848,9 +864,15 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, continue } + auxLeaf := fn.ChainOption( + func(leaves input.HtlcAuxLeaves) input.AuxTapLeaf { + return leaves[htlc.HtlcIndex].AuxTapLeaf + }, + )(incomingAuxLeaves) + err := addHTLC( commitTx, whoseCommit, true, htlc, keyRing, - cb.chanState.ChanType, + cb.chanState.ChanType, auxLeaf, ) if err != nil { return nil, err @@ -1128,7 +1150,7 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType, // channel. func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing, -) (*input.HtlcScriptTree, error) { + auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) { var ( htlcScriptTree *input.HtlcScriptTree @@ -1145,8 +1167,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, case isIncoming && whoseCommit.IsLocal(): htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:], whoseCommit, - input.NoneTapLeaf(), + keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, ) // We're being paid via an HTLC by the remote party, and the HTLC is @@ -1155,8 +1176,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, case isIncoming && whoseCommit.IsRemote(): htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, - keyRing.RevocationKey, rHash[:], whoseCommit, - input.NoneTapLeaf(), + keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, ) // We're sending an HTLC which is being added to our commitment @@ -1165,8 +1185,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, case !isIncoming && whoseCommit.IsLocal(): htlcScriptTree, err = input.SenderHTLCScriptTaproot( keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, rHash[:], whoseCommit, - input.NoneTapLeaf(), + keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, ) // Finally, we're paying the remote party via an HTLC, which is being @@ -1175,8 +1194,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, case !isIncoming && whoseCommit.IsRemote(): htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, - keyRing.RevocationKey, rHash[:], whoseCommit, - input.NoneTapLeaf(), + keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf, ) } @@ -1191,7 +1209,8 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, // along side the multiplexer. func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool, whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte, - keyRing *CommitmentKeyRing) (input.ScriptDescriptor, error) { + keyRing *CommitmentKeyRing, + auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) { if !chanType.IsTaproot() { return genSegwitV0HtlcScript( @@ -1201,7 +1220,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool, } return GenTaprootHtlcScript( - isIncoming, whoseCommit, timeout, rHash, keyRing, + isIncoming, whoseCommit, timeout, rHash, keyRing, auxLeaf, ) } @@ -1214,13 +1233,15 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool, // the descriptor itself. func addHTLC(commitTx *wire.MsgTx, whoseCommit lntypes.ChannelParty, isIncoming bool, paymentDesc *PaymentDescriptor, - keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error { + keyRing *CommitmentKeyRing, chanType channeldb.ChannelType, + auxLeaf input.AuxTapLeaf) error { timeout := paymentDesc.Timeout rHash := paymentDesc.RHash scriptInfo, err := genHtlcScript( chanType, isIncoming, whoseCommit, timeout, rHash, keyRing, + auxLeaf, ) if err != nil { return err diff --git a/lnwallet/transactions.go b/lnwallet/transactions.go index 1cf954d3c..da86650bc 100644 --- a/lnwallet/transactions.go +++ b/lnwallet/transactions.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/input" ) const ( @@ -50,8 +51,8 @@ var ( // - func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay, - leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) ( - *wire.MsgTx, error) { + leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey, + auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this // spends an output with a CSV timeout). @@ -71,7 +72,7 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, // HTLC outputs. scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, + leaseExpiry, auxLeaf, ) if err != nil { return nil, err @@ -110,7 +111,8 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, cltvExpiry, csvDelay, leaseExpiry uint32, - revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { + revocationKey, delayKey *btcec.PublicKey, + auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) { // Create a version two transaction (as the success version of this // spends an output with a CSV timeout), and set the lock-time to the @@ -134,7 +136,7 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, // HTLC outputs. scriptInfo, err := SecondLevelHtlcScript( chanType, initiator, revocationKey, delayKey, csvDelay, - leaseExpiry, + leaseExpiry, auxLeaf, ) if err != nil { return nil, err